From 91f08b944d466b4b7792ae4faef5e224f5efcc58 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Sun, 15 Feb 2026 19:06:15 +0000 Subject: [PATCH] ui improvements --- .../ghostwriter/GhostwriterPanel.tsx | 86 ++++++++----- .../components/ghostwriter/MessageList.tsx | 2 +- .../components/ghostwriter/ThreadList.tsx | 117 +++++++++++++++--- 3 files changed, 151 insertions(+), 54 deletions(-) diff --git a/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx b/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx index c3a73b1..661b9af 100644 --- a/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx +++ b/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx @@ -4,11 +4,9 @@ import type { JobChatStreamEvent, JobChatThread, } from "@shared/types"; -import { MessageSquare } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import * as api from "../../api"; import { Composer } from "./Composer"; import { MessageList } from "./MessageList"; @@ -23,6 +21,9 @@ export const GhostwriterPanel: React.FC = ({ job }) => { const [threads, setThreads] = useState([]); const [activeThreadId, setActiveThreadId] = useState(null); const [messages, setMessages] = useState([]); + const [threadPreviews, setThreadPreviews] = useState>( + {}, + ); const [isLoading, setIsLoading] = useState(true); const [isStreaming, setIsStreaming] = useState(false); const [streamingMessageId, setStreamingMessageId] = useState( @@ -54,6 +55,12 @@ export const GhostwriterPanel: React.FC = ({ job }) => { limit: 300, }); setMessages(data.messages); + const preview = [...data.messages] + .reverse() + .find((message) => !!message.content.trim())?.content; + if (preview) { + setThreadPreviews((current) => ({ ...current, [threadId]: preview })); + } }, [job.id], ); @@ -63,6 +70,7 @@ export const GhostwriterPanel: React.FC = ({ job }) => { title: `${job.title} @ ${job.employer}`, }); setThreads((current) => [created.thread, ...current]); + setThreadPreviews((current) => ({ ...current, [created.thread.id]: "" })); setActiveThreadId(created.thread.id); setMessages([]); return created.thread; @@ -144,6 +152,13 @@ export const GhostwriterPanel: React.FC = ({ job }) => { : message, ), ); + const threadId = activeThreadIdRef.current; + if (threadId) { + setThreadPreviews((current) => ({ + ...current, + [threadId]: `${current[threadId] ?? ""}${event.delta}`.trim(), + })); + } return; } @@ -159,6 +174,10 @@ export const GhostwriterPanel: React.FC = ({ job }) => { setStreamingMessageId(null); setActiveRunId(null); setIsStreaming(false); + setThreadPreviews((current) => ({ + ...current, + [event.message.threadId]: event.message.content, + })); return; } @@ -193,6 +212,7 @@ export const GhostwriterPanel: React.FC = ({ job }) => { }; setMessages((current) => [...current, optimisticUser]); + setThreadPreviews((current) => ({ ...current, [threadId]: content })); setIsStreaming(true); const controller = new AbortController(); @@ -287,16 +307,12 @@ export const GhostwriterPanel: React.FC = ({ job }) => { ]); return ( - - - - - Ghostwriter - - - +
+
{ setActiveThreadId(threadId); @@ -308,29 +324,33 @@ export const GhostwriterPanel: React.FC = ({ job }) => { disabled={isLoading || isStreaming} /> -
- +
+
+ +
+ +
+ + + +
- - - - - - +
+
); }; diff --git a/orchestrator/src/client/components/ghostwriter/MessageList.tsx b/orchestrator/src/client/components/ghostwriter/MessageList.tsx index 5c4e29c..92abb40 100644 --- a/orchestrator/src/client/components/ghostwriter/MessageList.tsx +++ b/orchestrator/src/client/components/ghostwriter/MessageList.tsx @@ -40,7 +40,7 @@ export const MessageList: React.FC = ({
{isUser ? "You" - : `Copilot${message.version > 1 ? ` v${message.version}` : ""}`} + : `Ghostwriter${message.version > 1 ? ` v${message.version}` : ""}`}
{isActiveStreaming ? ( diff --git a/orchestrator/src/client/components/ghostwriter/ThreadList.tsx b/orchestrator/src/client/components/ghostwriter/ThreadList.tsx index 07a7414..681cb82 100644 --- a/orchestrator/src/client/components/ghostwriter/ThreadList.tsx +++ b/orchestrator/src/client/components/ghostwriter/ThreadList.tsx @@ -1,27 +1,71 @@ -import type { JobChatThread } from "@shared/types"; +import type { Job, JobChatThread } from "@shared/types"; import type React from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; type ThreadListProps = { + job: Job; threads: JobChatThread[]; + previews: Record; activeThreadId: string | null; onSelectThread: (threadId: string) => void; onCreateThread: () => void; disabled?: boolean; }; +function formatRelativeTime(value: string | null): string { + if (!value) return "Updated just now"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "Updated recently"; + const diffMs = date.getTime() - Date.now(); + const absMs = Math.abs(diffMs); + + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); + + if (absMs < hour) { + const minutes = Math.max(1, Math.round(diffMs / minute)); + return `Updated ${rtf.format(minutes, "minute")}`; + } + if (absMs < day) { + const hours = Math.round(diffMs / hour); + return `Updated ${rtf.format(hours, "hour")}`; + } + const days = Math.round(diffMs / day); + return `Updated ${rtf.format(days, "day")}`; +} + +function normalizeThreadTitle(input: string | null, fallback: string): string { + const value = input?.trim(); + return value && value.length > 0 ? value : fallback; +} + export const ThreadList: React.FC = ({ + job, threads, + previews, activeThreadId, onSelectThread, onCreateThread, disabled, }) => { + const titleCounts = new Map(); + threads.forEach((thread) => { + const normalized = normalizeThreadTitle( + thread.title, + `${job.title} @ ${job.employer}`, + ); + titleCounts.set(normalized, (titleCounts.get(normalized) ?? 0) + 1); + }); + + const seenTitles = new Map(); + return ( -
+ ); };