Skip to content

Commit a1ff19d

Browse files
JSv4claude
andcommitted
Add chat interface components: ResearchCard, ProjectPicker, SourcesPanel
- ResearchCard: Analysis card with user question, AI response status, sources badge, expandable steps list, and research sections - ProjectPicker: Vault/project dropdown selector with search and support for knowledge-base, folder, workspace, and vault types - SourcesPanel: Tabbed panel for files/sources with search and integration actions (upload, iManage, NetDocuments, SharePoint) Includes full Storybook stories for all components. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 79d3e89 commit a1ff19d

13 files changed

Lines changed: 2424 additions & 0 deletions
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import React, { useState } from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import { ProjectPicker, ProjectItem, ProjectSearch } from './ProjectPicker';
4+
5+
const meta: Meta<typeof ProjectPicker> = {
6+
title: 'Chat/ProjectPicker',
7+
component: ProjectPicker,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['autodocs'],
12+
};
13+
14+
export default meta;
15+
type Story = StoryObj<typeof ProjectPicker>;
16+
17+
export const Default: Story = {
18+
render: () => {
19+
const [selected, setSelected] = useState('kb-1');
20+
21+
return (
22+
<div style={{ width: 400 }}>
23+
<ProjectPicker
24+
title="Choose Vault Project"
25+
subtitle="3 projects available"
26+
>
27+
<ProjectItem
28+
id="kb-1"
29+
name="Whitford Lane Knowledge Base"
30+
description="Internal knowledge base"
31+
type="knowledge-base"
32+
selected={selected === 'kb-1'}
33+
onSelect={setSelected}
34+
/>
35+
<ProjectItem
36+
id="folder-1"
37+
name="Merger Agreements"
38+
description="3,902 files"
39+
type="folder"
40+
selected={selected === 'folder-1'}
41+
onSelect={setSelected}
42+
/>
43+
<ProjectItem
44+
id="folder-2"
45+
name="Commercial Contracts"
46+
type="folder"
47+
selected={selected === 'folder-2'}
48+
onSelect={setSelected}
49+
/>
50+
</ProjectPicker>
51+
</div>
52+
);
53+
},
54+
};
55+
56+
export const WithSearch: Story = {
57+
render: () => {
58+
const [selected, setSelected] = useState('');
59+
const [search, setSearch] = useState('');
60+
61+
const projects = [
62+
{ id: 'kb-1', name: 'Whitford Lane Knowledge Base', description: 'Internal knowledge base', type: 'knowledge-base' as const },
63+
{ id: 'folder-1', name: 'Merger Agreements', description: '3,902 files', type: 'folder' as const },
64+
{ id: 'folder-2', name: 'Commercial Contracts', description: '1,247 files', type: 'folder' as const },
65+
{ id: 'vault-1', name: 'Client Confidential', description: 'Restricted access', type: 'vault' as const },
66+
{ id: 'workspace-1', name: 'Due Diligence Workspace', description: '45 active matters', type: 'workspace' as const },
67+
];
68+
69+
const filteredProjects = projects.filter(p =>
70+
p.name.toLowerCase().includes(search.toLowerCase()) ||
71+
p.description?.toLowerCase().includes(search.toLowerCase())
72+
);
73+
74+
return (
75+
<div style={{ width: 400 }}>
76+
<ProjectPicker
77+
title="Choose Vault Project"
78+
subtitle={`${projects.length} projects available`}
79+
>
80+
<ProjectSearch
81+
value={search}
82+
onChange={setSearch}
83+
placeholder="Search projects..."
84+
/>
85+
{filteredProjects.map(project => (
86+
<ProjectItem
87+
key={project.id}
88+
id={project.id}
89+
name={project.name}
90+
description={project.description}
91+
type={project.type}
92+
selected={selected === project.id}
93+
onSelect={setSelected}
94+
/>
95+
))}
96+
</ProjectPicker>
97+
</div>
98+
);
99+
},
100+
};
101+
102+
export const Closed: Story = {
103+
render: () => (
104+
<div style={{ width: 400 }}>
105+
<ProjectPicker
106+
title="Choose Vault Project"
107+
subtitle="3 projects available"
108+
defaultOpen={false}
109+
>
110+
<ProjectItem
111+
id="kb-1"
112+
name="Whitford Lane Knowledge Base"
113+
description="Internal knowledge base"
114+
type="knowledge-base"
115+
/>
116+
<ProjectItem
117+
id="folder-1"
118+
name="Merger Agreements"
119+
description="3,902 files"
120+
type="folder"
121+
/>
122+
<ProjectItem
123+
id="folder-2"
124+
name="Commercial Contracts"
125+
type="folder"
126+
/>
127+
</ProjectPicker>
128+
</div>
129+
),
130+
};
131+
132+
export const AllProjectTypes: Story = {
133+
render: () => {
134+
const [selected, setSelected] = useState('');
135+
136+
return (
137+
<div style={{ width: 400 }}>
138+
<ProjectPicker
139+
title="All Project Types"
140+
subtitle="4 projects available"
141+
>
142+
<ProjectItem
143+
id="kb"
144+
name="Knowledge Base"
145+
description="Shared team knowledge"
146+
type="knowledge-base"
147+
selected={selected === 'kb'}
148+
onSelect={setSelected}
149+
/>
150+
<ProjectItem
151+
id="folder"
152+
name="Document Folder"
153+
description="1,234 files"
154+
type="folder"
155+
selected={selected === 'folder'}
156+
onSelect={setSelected}
157+
/>
158+
<ProjectItem
159+
id="workspace"
160+
name="Active Workspace"
161+
description="Current matters"
162+
type="workspace"
163+
selected={selected === 'workspace'}
164+
onSelect={setSelected}
165+
/>
166+
<ProjectItem
167+
id="vault"
168+
name="Secure Vault"
169+
description="Encrypted storage"
170+
type="vault"
171+
selected={selected === 'vault'}
172+
onSelect={setSelected}
173+
/>
174+
</ProjectPicker>
175+
</div>
176+
);
177+
},
178+
};
179+
180+
export const SingleItem: Story = {
181+
render: () => (
182+
<div style={{ width: 400 }}>
183+
<ProjectItem
184+
id="kb-1"
185+
name="Whitford Lane Knowledge Base"
186+
description="Internal knowledge base"
187+
type="knowledge-base"
188+
selected
189+
/>
190+
</div>
191+
),
192+
};
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
export const projectPickerStyles = `
2+
/* ============ Project Picker Container ============ */
3+
.oc-project-picker {
4+
display: flex;
5+
flex-direction: column;
6+
background: var(--oc-bg-canvas, #FFFFFF);
7+
border-radius: var(--oc-radius-lg, 8px);
8+
box-shadow: var(--oc-shadow-lg);
9+
overflow: hidden;
10+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
11+
}
12+
13+
.oc-project-picker__header {
14+
display: flex;
15+
align-items: center;
16+
justify-content: space-between;
17+
width: 100%;
18+
padding: 16px 20px;
19+
border: none;
20+
border-bottom: 1px solid transparent;
21+
background: transparent;
22+
cursor: pointer;
23+
text-align: left;
24+
transition: all 0.15s ease;
25+
}
26+
27+
.oc-project-picker--open .oc-project-picker__header {
28+
border-bottom-color: var(--oc-border-default, #E2E8F0);
29+
}
30+
31+
.oc-project-picker__header:hover {
32+
background: var(--oc-bg-surface, #F8FAFC);
33+
}
34+
35+
.oc-project-picker__header-content {
36+
display: flex;
37+
flex-direction: column;
38+
gap: 2px;
39+
}
40+
41+
.oc-project-picker__title {
42+
font-size: 15px;
43+
font-weight: 600;
44+
color: var(--oc-fg-primary, #0F172A);
45+
}
46+
47+
.oc-project-picker__subtitle {
48+
font-size: 13px;
49+
color: var(--oc-fg-tertiary, #94A3B8);
50+
}
51+
52+
.oc-project-picker__chevron {
53+
flex-shrink: 0;
54+
color: var(--oc-fg-tertiary, #94A3B8);
55+
transition: transform 0.2s ease;
56+
}
57+
58+
.oc-project-picker--open .oc-project-picker__chevron {
59+
transform: rotate(180deg);
60+
}
61+
62+
.oc-project-picker__list {
63+
display: flex;
64+
flex-direction: column;
65+
padding: 8px;
66+
}
67+
68+
/* ============ Project Item ============ */
69+
.oc-project-item {
70+
display: flex;
71+
align-items: center;
72+
justify-content: space-between;
73+
gap: 12px;
74+
padding: 12px 14px;
75+
border-radius: var(--oc-radius-md, 6px);
76+
cursor: pointer;
77+
transition: all 0.15s ease;
78+
outline: none;
79+
}
80+
81+
.oc-project-item:hover {
82+
background: var(--oc-bg-surface, #F8FAFC);
83+
}
84+
85+
.oc-project-item:focus-visible {
86+
box-shadow: 0 0 0 2px var(--oc-accent, #0891B2);
87+
}
88+
89+
.oc-project-item--selected {
90+
background: var(--oc-bg-surface, #F8FAFC);
91+
box-shadow: inset 0 0 0 1px var(--oc-border-default, #E2E8F0);
92+
}
93+
94+
.oc-project-item--selected:hover {
95+
background: var(--oc-bg-surface-hover, #F1F5F9);
96+
}
97+
98+
.oc-project-item__content {
99+
flex: 1;
100+
min-width: 0;
101+
display: flex;
102+
flex-direction: column;
103+
gap: 2px;
104+
}
105+
106+
.oc-project-item__name {
107+
font-size: 15px;
108+
font-weight: 500;
109+
color: var(--oc-fg-primary, #0F172A);
110+
white-space: nowrap;
111+
overflow: hidden;
112+
text-overflow: ellipsis;
113+
}
114+
115+
.oc-project-item__desc {
116+
font-size: 13px;
117+
color: var(--oc-fg-tertiary, #94A3B8);
118+
white-space: nowrap;
119+
overflow: hidden;
120+
text-overflow: ellipsis;
121+
}
122+
123+
.oc-project-item__icon {
124+
flex-shrink: 0;
125+
width: 32px;
126+
height: 32px;
127+
display: flex;
128+
align-items: center;
129+
justify-content: center;
130+
color: var(--oc-fg-tertiary, #94A3B8);
131+
transition: color 0.15s ease;
132+
}
133+
134+
.oc-project-item:hover .oc-project-item__icon {
135+
color: var(--oc-fg-secondary, #475569);
136+
}
137+
138+
.oc-project-item--selected .oc-project-item__icon {
139+
color: var(--oc-accent, #0891B2);
140+
}
141+
142+
/* ============ Project Search ============ */
143+
.oc-project-search {
144+
display: flex;
145+
align-items: center;
146+
gap: 10px;
147+
padding: 10px 14px;
148+
margin: 8px;
149+
background: var(--oc-bg-surface, #F8FAFC);
150+
border-radius: var(--oc-radius-md, 6px);
151+
border: 1px solid transparent;
152+
transition: all 0.15s ease;
153+
}
154+
155+
.oc-project-search:focus-within {
156+
background: var(--oc-bg-canvas, #FFFFFF);
157+
border-color: var(--oc-accent, #0891B2);
158+
box-shadow: 0 0 0 3px rgba(8, 145, 178, 0.15);
159+
}
160+
161+
.oc-project-search__icon {
162+
flex-shrink: 0;
163+
color: var(--oc-fg-tertiary, #94A3B8);
164+
}
165+
166+
.oc-project-search__input {
167+
flex: 1;
168+
min-width: 0;
169+
padding: 0;
170+
border: none;
171+
background: transparent;
172+
font-family: inherit;
173+
font-size: 14px;
174+
color: var(--oc-fg-primary, #0F172A);
175+
outline: none;
176+
}
177+
178+
.oc-project-search__input::placeholder {
179+
color: var(--oc-fg-tertiary, #94A3B8);
180+
}
181+
`;

0 commit comments

Comments
 (0)