Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 67 additions & 41 deletions apps/web/src/components/sme/SmeChatWorkspace.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { BookOpenIcon, SendIcon } from "lucide-react";
import { BookOpenIcon, ArrowUpIcon, SparklesIcon } from "lucide-react";
import type { SmeConversationId, SmeMessage, SmeMessageId } from "@okcode/contracts";
import { ensureNativeApi } from "~/nativeApi";
import { useSmeStore } from "~/smeStore";
Expand Down Expand Up @@ -39,6 +39,14 @@ export function SmeChatWorkspace({
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, streamingText]);

// Auto-resize textarea
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
textarea.style.height = "auto";
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
}, [inputText]);

const handleSend = useCallback(async () => {
if (!conversationId || !inputText.trim() || sending) return;

Expand Down Expand Up @@ -106,29 +114,32 @@ export function SmeChatWorkspace({

if (!conversationId) {
return (
<div className="flex h-full items-center justify-center">
<div className="space-y-3 text-center">
<BookOpenIcon className="mx-auto size-10 text-muted-foreground/20" />
<p className="text-sm text-muted-foreground">
Select a conversation or create a new one to start chatting
<div className="flex h-full flex-col items-center justify-center gap-4">
<div className="flex size-14 items-center justify-center rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5">
<SparklesIcon className="size-7 text-primary/60" />
</div>
<div className="space-y-2 text-center">
<h3 className="text-base font-medium text-foreground">SME Chat</h3>
<p className="max-w-xs text-sm text-muted-foreground">
Select a conversation or create a new one to start chatting with your subject matter
expert.
</p>
</div>
</div>
);
}

return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-4 py-2">
<span className="text-sm font-medium text-foreground">Conversation</span>
<div className="flex h-full flex-col bg-background">
{/* Minimal Header */}
<div className="flex items-center justify-end px-4 py-2">
<button
type="button"
onClick={onToggleKnowledge}
className={`flex items-center gap-1.5 rounded-md px-2 py-1 text-xs transition-colors ${
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors ${
knowledgePanelOpen
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
>
<BookOpenIcon className="size-3.5" />
Expand All @@ -137,8 +148,8 @@ export function SmeChatWorkspace({
</div>

{/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-4">
<div className="mx-auto max-w-3xl space-y-4">
<div className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-3xl">
{messages.map((msg) => (
<SmeMessageBubble key={msg.messageId} message={msg} />
))}
Expand All @@ -158,39 +169,54 @@ export function SmeChatWorkspace({
/>
) : null}
{sending && !streamingText ? (
<div className="flex items-center gap-2 py-2">
<div className="flex gap-1">
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/40 [animation-delay:0ms]" />
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/40 [animation-delay:150ms]" />
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/40 [animation-delay:300ms]" />
<div className="flex items-center gap-4 px-4 py-5">
<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">
<SparklesIcon className="size-4" />
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">SME Assistant</p>
<div className="flex items-center gap-1.5">
<div className="flex gap-1">
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/50 [animation-delay:0ms]" />
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/50 [animation-delay:150ms]" />
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/50 [animation-delay:300ms]" />
</div>
<span className="text-xs text-muted-foreground">Thinking...</span>
</div>
</div>
<span className="text-xs text-muted-foreground">Thinking...</span>
</div>
) : null}
<div ref={messagesEndRef} />
</div>
</div>

{/* Composer */}
<div className="border-t border-border px-4 py-3">
<div className="mx-auto flex max-w-3xl items-end gap-2">
<textarea
ref={textareaRef}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask your subject matter expert..."
rows={1}
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"
/>
<button
type="button"
onClick={() => void handleSend()}
disabled={!inputText.trim() || sending}
className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
<SendIcon className="size-4" />
</button>
{/* Modern Composer */}
<div className="px-4 pb-4 pt-2">
<div className="mx-auto max-w-3xl">
<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">
<textarea
ref={textareaRef}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Message your SME..."
rows={1}
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"
/>
<div className="flex items-center gap-1 p-2">
<button
type="button"
onClick={() => void handleSend()}
disabled={!inputText.trim() || sending}
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"
>
<ArrowUpIcon className="size-4" />
</button>
</div>
</div>
<p className="mt-1.5 text-center text-[10px] text-muted-foreground/40">
SME can make mistakes. Verify important information.
</p>
</div>
</div>
</div>
Expand Down
52 changes: 27 additions & 25 deletions apps/web/src/components/sme/SmeConversationRail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useState } from "react";
import { MessageSquarePlusIcon, TrashIcon } from "lucide-react";
import { PlusIcon, TrashIcon, MessageSquareIcon } from "lucide-react";
import type { SmeConversationId } from "@okcode/contracts";

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

return (
<div className="flex w-56 shrink-0 flex-col border-r border-border bg-sidebar">
<div className="flex w-64 shrink-0 flex-col bg-muted/30">
{/* Header with new chat button */}
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm font-semibold text-foreground">Chats</span>
<button
type="button"
onClick={handleNewConversation}
disabled={creating}
className="flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
title="New conversation"
>
<PlusIcon className="size-4" />
</button>
</div>

{/* Project selector */}
{projects.length > 1 ? (
<div className="border-b border-border px-3 py-2">
<div className="px-3 pb-2">
<select
value={selectedProjectId ?? ""}
onChange={(e) => onProjectChange(e.target.value)}
className="w-full rounded-md border border-border bg-background px-2 py-1 text-xs"
className="w-full rounded-lg border border-border bg-background px-2.5 py-1.5 text-xs"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
Expand All @@ -73,25 +87,13 @@ export function SmeConversationRail({
</div>
) : null}

{/* New conversation button */}
<div className="px-3 py-2">
<button
type="button"
onClick={handleNewConversation}
disabled={creating}
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"
>
<MessageSquarePlusIcon className="size-3.5" />
<span>New Conversation</span>
</button>
</div>

{/* Conversation list */}
<div className="flex-1 overflow-y-auto px-1.5">
<div className="flex-1 overflow-y-auto px-2">
{conversations.length === 0 ? (
<p className="px-2 py-4 text-center text-xs text-muted-foreground">
No conversations yet
</p>
<div className="flex flex-col items-center gap-2 px-2 py-8 text-center">
<MessageSquareIcon className="size-5 text-muted-foreground/30" />
<p className="text-xs text-muted-foreground">No conversations yet</p>
</div>
) : (
<div className="space-y-0.5 py-1">
{conversations.map((conv) => (
Expand All @@ -100,17 +102,17 @@ export function SmeConversationRail({
type="button"
onClick={() => setActiveConversationId(conv.conversationId)}
className={cn(
"group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors",
"group flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left text-[13px] transition-colors",
activeConversationId === conv.conversationId
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
)}
>
<span className="min-w-0 flex-1 truncate">{conv.title}</span>
<button
type="button"
onClick={(e) => void handleDeleteConversation(e, conv.conversationId)}
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
className="shrink-0 rounded-md p-0.5 opacity-0 transition-opacity hover:bg-background group-hover:opacity-100"
>
<TrashIcon className="size-3 text-muted-foreground hover:text-destructive" />
</button>
Expand Down
43 changes: 23 additions & 20 deletions apps/web/src/components/sme/SmeKnowledgePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,21 @@ export function SmeKnowledgePanel({ project, onClose }: SmeKnowledgePanelProps)
);

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

{/* Upload button */}
<div className="px-3 py-2">
<div className="px-3 pb-2">
<input
ref={fileInputRef}
type="file"
Expand All @@ -112,42 +112,45 @@ export function SmeKnowledgePanel({ project, onClose }: SmeKnowledgePanelProps)
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
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"
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"
>
<UploadIcon className="size-3.5" />
<UploadIcon className="size-4" />
<span>{uploading ? "Uploading..." : "Upload Document"}</span>
</button>
<p className="mt-1 text-center text-[10px] text-muted-foreground/60">
<p className="mt-1.5 text-center text-[10px] text-muted-foreground/50">
.txt, .md, .json, .csv, .yaml, .html, .xml
</p>
</div>

{/* Document list */}
<div className="flex-1 overflow-y-auto px-2">
{documents.length === 0 ? (
<p className="px-2 py-6 text-center text-xs text-muted-foreground">
No documents uploaded yet.
<br />
Upload reference docs to give your SME context.
</p>
<div className="flex flex-col items-center gap-2 px-2 py-8 text-center">
<FileTextIcon className="size-5 text-muted-foreground/30" />
<p className="text-xs text-muted-foreground">
No documents uploaded yet.
<br />
Upload reference docs to give your SME context.
</p>
</div>
) : (
<div className="space-y-1 py-1">
<div className="space-y-0.5 py-1">
{documents.map((doc) => (
<div
key={doc.documentId}
className="group flex items-start gap-2 rounded-md px-2 py-1.5 hover:bg-accent/50"
className="group flex items-start gap-2.5 rounded-lg px-3 py-2 transition-colors hover:bg-muted/60"
>
<FileTextIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
<FileTextIcon className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="truncate text-xs font-medium text-foreground">{doc.title}</p>
<p className="truncate text-[10px] text-muted-foreground">
<p className="truncate text-[13px] font-medium text-foreground">{doc.title}</p>
<p className="truncate text-[11px] text-muted-foreground">
{doc.fileName} &middot; {formatBytes(doc.sizeBytes)}
</p>
</div>
<button
type="button"
onClick={() => void handleDelete(doc.documentId)}
className="mt-0.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
className="mt-0.5 shrink-0 rounded-md p-0.5 opacity-0 transition-opacity hover:bg-background group-hover:opacity-100"
>
<TrashIcon className="size-3 text-muted-foreground hover:text-destructive" />
</button>
Expand Down
Loading
Loading