Skip to content

Commit 984a19c

Browse files
authored
Refresh SME chat with modern sidebar and composer (#385)
- Restyle the SME workspace with a cleaner chat layout, auto-sizing composer, and updated empty/loading states - Update conversation and knowledge panels with tighter spacing, clearer actions, and improved visual hierarchy - Rework message bubbles to show speaker labels, refined avatars, and a better streaming cursor
1 parent e06dab0 commit 984a19c

4 files changed

Lines changed: 148 additions & 104 deletions

File tree

apps/web/src/components/sme/SmeChatWorkspace.tsx

Lines changed: 67 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
2-
import { BookOpenIcon, SendIcon } from "lucide-react";
2+
import { BookOpenIcon, ArrowUpIcon, SparklesIcon } from "lucide-react";
33
import type { SmeConversationId, SmeMessage, SmeMessageId } from "@okcode/contracts";
44
import { ensureNativeApi } from "~/nativeApi";
55
import { useSmeStore } from "~/smeStore";
@@ -39,6 +39,14 @@ export function SmeChatWorkspace({
3939
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
4040
}, [messages, streamingText]);
4141

42+
// Auto-resize textarea
43+
useEffect(() => {
44+
const textarea = textareaRef.current;
45+
if (!textarea) return;
46+
textarea.style.height = "auto";
47+
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
48+
}, [inputText]);
49+
4250
const handleSend = useCallback(async () => {
4351
if (!conversationId || !inputText.trim() || sending) return;
4452

@@ -106,29 +114,32 @@ export function SmeChatWorkspace({
106114

107115
if (!conversationId) {
108116
return (
109-
<div className="flex h-full items-center justify-center">
110-
<div className="space-y-3 text-center">
111-
<BookOpenIcon className="mx-auto size-10 text-muted-foreground/20" />
112-
<p className="text-sm text-muted-foreground">
113-
Select a conversation or create a new one to start chatting
117+
<div className="flex h-full flex-col items-center justify-center gap-4">
118+
<div className="flex size-14 items-center justify-center rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5">
119+
<SparklesIcon className="size-7 text-primary/60" />
120+
</div>
121+
<div className="space-y-2 text-center">
122+
<h3 className="text-base font-medium text-foreground">SME Chat</h3>
123+
<p className="max-w-xs text-sm text-muted-foreground">
124+
Select a conversation or create a new one to start chatting with your subject matter
125+
expert.
114126
</p>
115127
</div>
116128
</div>
117129
);
118130
}
119131

120132
return (
121-
<div className="flex h-full flex-col">
122-
{/* Header */}
123-
<div className="flex items-center justify-between border-b border-border px-4 py-2">
124-
<span className="text-sm font-medium text-foreground">Conversation</span>
133+
<div className="flex h-full flex-col bg-background">
134+
{/* Minimal Header */}
135+
<div className="flex items-center justify-end px-4 py-2">
125136
<button
126137
type="button"
127138
onClick={onToggleKnowledge}
128-
className={`flex items-center gap-1.5 rounded-md px-2 py-1 text-xs transition-colors ${
139+
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors ${
129140
knowledgePanelOpen
130-
? "bg-accent text-accent-foreground"
131-
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
141+
? "bg-primary/10 text-primary"
142+
: "text-muted-foreground hover:bg-muted hover:text-foreground"
132143
}`}
133144
>
134145
<BookOpenIcon className="size-3.5" />
@@ -137,8 +148,8 @@ export function SmeChatWorkspace({
137148
</div>
138149

139150
{/* Messages */}
140-
<div className="flex-1 overflow-y-auto px-4 py-4">
141-
<div className="mx-auto max-w-3xl space-y-4">
151+
<div className="flex-1 overflow-y-auto">
152+
<div className="mx-auto max-w-3xl">
142153
{messages.map((msg) => (
143154
<SmeMessageBubble key={msg.messageId} message={msg} />
144155
))}
@@ -158,39 +169,54 @@ export function SmeChatWorkspace({
158169
/>
159170
) : null}
160171
{sending && !streamingText ? (
161-
<div className="flex items-center gap-2 py-2">
162-
<div className="flex gap-1">
163-
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/40 [animation-delay:0ms]" />
164-
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/40 [animation-delay:150ms]" />
165-
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/40 [animation-delay:300ms]" />
172+
<div className="flex items-center gap-4 px-4 py-5">
173+
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-primary/80 to-primary text-primary-foreground">
174+
<SparklesIcon className="size-4" />
175+
</div>
176+
<div className="space-y-1">
177+
<p className="text-xs font-medium text-muted-foreground">SME Assistant</p>
178+
<div className="flex items-center gap-1.5">
179+
<div className="flex gap-1">
180+
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/50 [animation-delay:0ms]" />
181+
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/50 [animation-delay:150ms]" />
182+
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/50 [animation-delay:300ms]" />
183+
</div>
184+
<span className="text-xs text-muted-foreground">Thinking...</span>
185+
</div>
166186
</div>
167-
<span className="text-xs text-muted-foreground">Thinking...</span>
168187
</div>
169188
) : null}
170189
<div ref={messagesEndRef} />
171190
</div>
172191
</div>
173192

174-
{/* Composer */}
175-
<div className="border-t border-border px-4 py-3">
176-
<div className="mx-auto flex max-w-3xl items-end gap-2">
177-
<textarea
178-
ref={textareaRef}
179-
value={inputText}
180-
onChange={(e) => setInputText(e.target.value)}
181-
onKeyDown={handleKeyDown}
182-
placeholder="Ask your subject matter expert..."
183-
rows={1}
184-
className="min-h-[36px] max-h-[200px] flex-1 resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring"
185-
/>
186-
<button
187-
type="button"
188-
onClick={() => void handleSend()}
189-
disabled={!inputText.trim() || sending}
190-
className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
191-
>
192-
<SendIcon className="size-4" />
193-
</button>
193+
{/* Modern Composer */}
194+
<div className="px-4 pb-4 pt-2">
195+
<div className="mx-auto max-w-3xl">
196+
<div className="relative flex items-end rounded-2xl border border-border bg-muted/30 shadow-sm transition-colors focus-within:border-ring focus-within:bg-muted/50">
197+
<textarea
198+
ref={textareaRef}
199+
value={inputText}
200+
onChange={(e) => setInputText(e.target.value)}
201+
onKeyDown={handleKeyDown}
202+
placeholder="Message your SME..."
203+
rows={1}
204+
className="max-h-[200px] min-h-[44px] flex-1 resize-none bg-transparent px-4 py-3 text-sm leading-relaxed outline-none placeholder:text-muted-foreground/60"
205+
/>
206+
<div className="flex items-center gap-1 p-2">
207+
<button
208+
type="button"
209+
onClick={() => void handleSend()}
210+
disabled={!inputText.trim() || sending}
211+
className="flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground transition-all hover:bg-primary/90 disabled:bg-muted-foreground/20 disabled:text-muted-foreground/40"
212+
>
213+
<ArrowUpIcon className="size-4" />
214+
</button>
215+
</div>
216+
</div>
217+
<p className="mt-1.5 text-center text-[10px] text-muted-foreground/40">
218+
SME can make mistakes. Verify important information.
219+
</p>
194220
</div>
195221
</div>
196222
</div>

apps/web/src/components/sme/SmeConversationRail.tsx

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useState } from "react";
2-
import { MessageSquarePlusIcon, TrashIcon } from "lucide-react";
2+
import { PlusIcon, TrashIcon, MessageSquareIcon } from "lucide-react";
33
import type { SmeConversationId } from "@okcode/contracts";
44

55
import type { Project } from "~/types";
@@ -55,14 +55,28 @@ export function SmeConversationRail({
5555
);
5656

5757
return (
58-
<div className="flex w-56 shrink-0 flex-col border-r border-border bg-sidebar">
58+
<div className="flex w-64 shrink-0 flex-col bg-muted/30">
59+
{/* Header with new chat button */}
60+
<div className="flex items-center justify-between px-4 py-3">
61+
<span className="text-sm font-semibold text-foreground">Chats</span>
62+
<button
63+
type="button"
64+
onClick={handleNewConversation}
65+
disabled={creating}
66+
className="flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
67+
title="New conversation"
68+
>
69+
<PlusIcon className="size-4" />
70+
</button>
71+
</div>
72+
5973
{/* Project selector */}
6074
{projects.length > 1 ? (
61-
<div className="border-b border-border px-3 py-2">
75+
<div className="px-3 pb-2">
6276
<select
6377
value={selectedProjectId ?? ""}
6478
onChange={(e) => onProjectChange(e.target.value)}
65-
className="w-full rounded-md border border-border bg-background px-2 py-1 text-xs"
79+
className="w-full rounded-lg border border-border bg-background px-2.5 py-1.5 text-xs"
6680
>
6781
{projects.map((p) => (
6882
<option key={p.id} value={p.id}>
@@ -73,25 +87,13 @@ export function SmeConversationRail({
7387
</div>
7488
) : null}
7589

76-
{/* New conversation button */}
77-
<div className="px-3 py-2">
78-
<button
79-
type="button"
80-
onClick={handleNewConversation}
81-
disabled={creating}
82-
className="flex w-full items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
83-
>
84-
<MessageSquarePlusIcon className="size-3.5" />
85-
<span>New Conversation</span>
86-
</button>
87-
</div>
88-
8990
{/* Conversation list */}
90-
<div className="flex-1 overflow-y-auto px-1.5">
91+
<div className="flex-1 overflow-y-auto px-2">
9192
{conversations.length === 0 ? (
92-
<p className="px-2 py-4 text-center text-xs text-muted-foreground">
93-
No conversations yet
94-
</p>
93+
<div className="flex flex-col items-center gap-2 px-2 py-8 text-center">
94+
<MessageSquareIcon className="size-5 text-muted-foreground/30" />
95+
<p className="text-xs text-muted-foreground">No conversations yet</p>
96+
</div>
9597
) : (
9698
<div className="space-y-0.5 py-1">
9799
{conversations.map((conv) => (
@@ -100,17 +102,17 @@ export function SmeConversationRail({
100102
type="button"
101103
onClick={() => setActiveConversationId(conv.conversationId)}
102104
className={cn(
103-
"group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors",
105+
"group flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left text-[13px] transition-colors",
104106
activeConversationId === conv.conversationId
105-
? "bg-accent text-accent-foreground"
106-
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
107+
? "bg-muted text-foreground"
108+
: "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
107109
)}
108110
>
109111
<span className="min-w-0 flex-1 truncate">{conv.title}</span>
110112
<button
111113
type="button"
112114
onClick={(e) => void handleDeleteConversation(e, conv.conversationId)}
113-
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
115+
className="shrink-0 rounded-md p-0.5 opacity-0 transition-opacity hover:bg-background group-hover:opacity-100"
114116
>
115117
<TrashIcon className="size-3 text-muted-foreground hover:text-destructive" />
116118
</button>

apps/web/src/components/sme/SmeKnowledgePanel.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,21 @@ export function SmeKnowledgePanel({ project, onClose }: SmeKnowledgePanelProps)
8686
);
8787

8888
return (
89-
<div className="flex w-72 shrink-0 flex-col border-l border-border bg-sidebar">
89+
<div className="flex w-72 shrink-0 flex-col border-l border-border bg-muted/30">
9090
{/* Header */}
91-
<div className="flex items-center justify-between border-b border-border px-3 py-2">
92-
<span className="text-xs font-medium text-foreground">Knowledge Base</span>
91+
<div className="flex items-center justify-between px-4 py-3">
92+
<span className="text-sm font-semibold text-foreground">Knowledge Base</span>
9393
<button
9494
type="button"
9595
onClick={onClose}
96-
className="rounded p-0.5 text-muted-foreground hover:text-foreground"
96+
className="flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
9797
>
98-
<XIcon className="size-3.5" />
98+
<XIcon className="size-4" />
9999
</button>
100100
</div>
101101

102102
{/* Upload button */}
103-
<div className="px-3 py-2">
103+
<div className="px-3 pb-2">
104104
<input
105105
ref={fileInputRef}
106106
type="file"
@@ -112,42 +112,45 @@ export function SmeKnowledgePanel({ project, onClose }: SmeKnowledgePanelProps)
112112
type="button"
113113
onClick={() => fileInputRef.current?.click()}
114114
disabled={uploading}
115-
className="flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-border px-3 py-2 text-xs text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground disabled:opacity-50"
115+
className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-border/60 px-3 py-3 text-xs text-muted-foreground transition-colors hover:border-primary/40 hover:bg-primary/5 hover:text-foreground disabled:opacity-50"
116116
>
117-
<UploadIcon className="size-3.5" />
117+
<UploadIcon className="size-4" />
118118
<span>{uploading ? "Uploading..." : "Upload Document"}</span>
119119
</button>
120-
<p className="mt-1 text-center text-[10px] text-muted-foreground/60">
120+
<p className="mt-1.5 text-center text-[10px] text-muted-foreground/50">
121121
.txt, .md, .json, .csv, .yaml, .html, .xml
122122
</p>
123123
</div>
124124

125125
{/* Document list */}
126126
<div className="flex-1 overflow-y-auto px-2">
127127
{documents.length === 0 ? (
128-
<p className="px-2 py-6 text-center text-xs text-muted-foreground">
129-
No documents uploaded yet.
130-
<br />
131-
Upload reference docs to give your SME context.
132-
</p>
128+
<div className="flex flex-col items-center gap-2 px-2 py-8 text-center">
129+
<FileTextIcon className="size-5 text-muted-foreground/30" />
130+
<p className="text-xs text-muted-foreground">
131+
No documents uploaded yet.
132+
<br />
133+
Upload reference docs to give your SME context.
134+
</p>
135+
</div>
133136
) : (
134-
<div className="space-y-1 py-1">
137+
<div className="space-y-0.5 py-1">
135138
{documents.map((doc) => (
136139
<div
137140
key={doc.documentId}
138-
className="group flex items-start gap-2 rounded-md px-2 py-1.5 hover:bg-accent/50"
141+
className="group flex items-start gap-2.5 rounded-lg px-3 py-2 transition-colors hover:bg-muted/60"
139142
>
140-
<FileTextIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
143+
<FileTextIcon className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
141144
<div className="min-w-0 flex-1">
142-
<p className="truncate text-xs font-medium text-foreground">{doc.title}</p>
143-
<p className="truncate text-[10px] text-muted-foreground">
145+
<p className="truncate text-[13px] font-medium text-foreground">{doc.title}</p>
146+
<p className="truncate text-[11px] text-muted-foreground">
144147
{doc.fileName} &middot; {formatBytes(doc.sizeBytes)}
145148
</p>
146149
</div>
147150
<button
148151
type="button"
149152
onClick={() => void handleDelete(doc.documentId)}
150-
className="mt-0.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
153+
className="mt-0.5 shrink-0 rounded-md p-0.5 opacity-0 transition-opacity hover:bg-background group-hover:opacity-100"
151154
>
152155
<TrashIcon className="size-3 text-muted-foreground hover:text-destructive" />
153156
</button>

0 commit comments

Comments
 (0)