From f9abd6ff740e96265aa5ceb1fc22e771780f75b8 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Sun, 15 Feb 2026 18:39:57 +0000 Subject: [PATCH] rename code --- .../components/ghostwriter/Composer.tsx | 62 +-- .../ghostwriter/GhostwriterPanel.tsx | 177 ++++--- .../components/ghostwriter/MessageList.tsx | 23 +- .../components/ghostwriter/ThreadList.tsx | 118 +---- .../client/components/job-chat/Composer.tsx | 52 -- .../components/job-chat/JobChatPanel.tsx | 336 ------------ .../components/job-chat/MessageList.tsx | 59 --- .../components/job-chat/RunControls.tsx | 45 -- .../components/job-chat/StreamingMessage.tsx | 16 - .../client/components/job-chat/ThreadList.tsx | 62 --- orchestrator/src/client/pages/JobPage.tsx | 5 +- orchestrator/src/server/api/routes.ts | 4 +- .../src/server/api/routes/ghostwriter.test.ts | 75 +-- .../src/server/api/routes/ghostwriter.ts | 352 +------------ .../src/server/api/routes/job-chat.test.ts | 154 ------ .../src/server/api/routes/job-chat.ts | 318 ----------- .../src/server/repositories/ghostwriter.ts | 39 -- .../src/server/repositories/job-chat.ts | 329 ------------ .../src/server/services/ghostwriter.ts | 159 +----- .../src/server/services/job-chat-context.ts | 193 ------- orchestrator/src/server/services/job-chat.ts | 498 ------------------ 21 files changed, 220 insertions(+), 2856 deletions(-) delete mode 100644 orchestrator/src/client/components/job-chat/Composer.tsx delete mode 100644 orchestrator/src/client/components/job-chat/JobChatPanel.tsx delete mode 100644 orchestrator/src/client/components/job-chat/MessageList.tsx delete mode 100644 orchestrator/src/client/components/job-chat/RunControls.tsx delete mode 100644 orchestrator/src/client/components/job-chat/StreamingMessage.tsx delete mode 100644 orchestrator/src/client/components/job-chat/ThreadList.tsx delete mode 100644 orchestrator/src/server/api/routes/job-chat.test.ts delete mode 100644 orchestrator/src/server/api/routes/job-chat.ts delete mode 100644 orchestrator/src/server/repositories/job-chat.ts delete mode 100644 orchestrator/src/server/services/job-chat-context.ts delete mode 100644 orchestrator/src/server/services/job-chat.ts diff --git a/orchestrator/src/client/components/ghostwriter/Composer.tsx b/orchestrator/src/client/components/ghostwriter/Composer.tsx index 9f1ecaa..167744d 100644 --- a/orchestrator/src/client/components/ghostwriter/Composer.tsx +++ b/orchestrator/src/client/components/ghostwriter/Composer.tsx @@ -1,5 +1,4 @@ -import { getMetaShortcutLabel, isMetaKeyPressed } from "@client/lib/meta-key"; -import { RefreshCcw, Send, Square } from "lucide-react"; +import { Send } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; @@ -7,21 +6,10 @@ import { Textarea } from "@/components/ui/textarea"; type ComposerProps = { disabled?: boolean; - isStreaming: boolean; - canRegenerate: boolean; - onRegenerate: () => Promise; - onStop: () => Promise; onSend: (content: string) => Promise; }; -export const Composer: React.FC = ({ - disabled, - isStreaming, - canRegenerate, - onRegenerate, - onStop, - onSend, -}) => { +export const Composer: React.FC = ({ disabled, onSend }) => { const [value, setValue] = useState(""); const submit = async () => { @@ -39,7 +27,7 @@ export const Composer: React.FC = ({ onChange={(event) => setValue(event.target.value)} disabled={disabled} onKeyDown={(event) => { - if (isMetaKeyPressed(event) && event.key === "Enter") { + if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { event.preventDefault(); void submit(); } @@ -48,42 +36,16 @@ export const Composer: React.FC = ({ />
- {getMetaShortcutLabel("Enter")} to send -
-
- {isStreaming ? ( - - ) : ( - - )} - - + Cmd/Ctrl+Enter to send
+
); diff --git a/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx b/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx index 244c545..c3a73b1 100644 --- a/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx +++ b/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx @@ -1,16 +1,27 @@ -import type { Job, JobChatMessage, JobChatStreamEvent } from "@shared/types"; +import type { + Job, + JobChatMessage, + 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"; +import { RunControls } from "./RunControls"; +import { ThreadList } from "./ThreadList"; type GhostwriterPanelProps = { job: Job; }; export const GhostwriterPanel: React.FC = ({ job }) => { + const [threads, setThreads] = useState([]); + const [activeThreadId, setActiveThreadId] = useState(null); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isStreaming, setIsStreaming] = useState(false); @@ -18,13 +29,16 @@ export const GhostwriterPanel: React.FC = ({ job }) => { null, ); const [activeRunId, setActiveRunId] = useState(null); - const messageListRef = useRef(null); + const streamAbortRef = useRef(null); - const messageCount = messages.length; + const activeThreadIdRef = useRef(null); + + useEffect(() => { + activeThreadIdRef.current = activeThreadId; + }, [activeThreadId]); useEffect(() => { - if (messageCount === 0) return; const container = messageListRef.current; if (!container) return; const distanceToBottom = @@ -32,19 +46,45 @@ export const GhostwriterPanel: React.FC = ({ job }) => { if (distanceToBottom < 120 || isStreaming) { container.scrollTop = container.scrollHeight; } - }, [messageCount, isStreaming]); + }); - const loadMessages = useCallback(async () => { - const data = await api.listJobGhostwriterMessages(job.id, { - limit: 300, + const loadThreadMessages = useCallback( + async (threadId: string) => { + const data = await api.listJobChatMessages(job.id, threadId, { + limit: 300, + }); + setMessages(data.messages); + }, + [job.id], + ); + + const createThread = useCallback(async () => { + const created = await api.createJobChatThread(job.id, { + title: `${job.title} @ ${job.employer}`, }); - setMessages(data.messages); - }, [job.id]); + setThreads((current) => [created.thread, ...current]); + setActiveThreadId(created.thread.id); + setMessages([]); + return created.thread; + }, [job.id, job.title, job.employer]); const load = useCallback(async () => { setIsLoading(true); try { - await loadMessages(); + const data = await api.listJobChatThreads(job.id); + const nextThreads = data.threads; + setThreads(nextThreads); + + let threadId = nextThreads[0]?.id ?? null; + if (!threadId) { + const created = await createThread(); + threadId = created.id; + } + + setActiveThreadId(threadId); + if (threadId) { + await loadThreadMessages(threadId); + } } catch (error) { const message = error instanceof Error ? error.message : "Failed to load Ghostwriter"; @@ -52,7 +92,7 @@ export const GhostwriterPanel: React.FC = ({ job }) => { } finally { setIsLoading(false); } - }, [loadMessages]); + }, [job.id, createThread, loadThreadMessages]); useEffect(() => { void load(); @@ -68,9 +108,8 @@ export const GhostwriterPanel: React.FC = ({ job }) => { setActiveRunId(event.runId); setStreamingMessageId(event.messageId); setMessages((current) => { - if (current.some((message) => message.id === event.messageId)) { + if (current.some((message) => message.id === event.messageId)) return current; - } return [ ...current, { @@ -135,11 +174,12 @@ export const GhostwriterPanel: React.FC = ({ job }) => { const sendMessage = useCallback( async (content: string) => { - if (isStreaming) return; + if (!activeThreadIdRef.current || isStreaming) return; + const threadId = activeThreadIdRef.current; const optimisticUser: JobChatMessage = { id: `tmp-user-${Date.now()}`, - threadId: messages[messages.length - 1]?.threadId || "pending-thread", + threadId, jobId: job.id, role: "user", content, @@ -159,13 +199,14 @@ export const GhostwriterPanel: React.FC = ({ job }) => { streamAbortRef.current = controller; try { - await api.streamJobGhostwriterMessage( + await api.streamJobChatMessage( job.id, + threadId, { content, signal: controller.signal }, { onEvent: onStreamEvent }, ); - await loadMessages(); + await loadThreadMessages(threadId); } catch (error) { if (error instanceof Error && error.name === "AbortError") { return; @@ -179,30 +220,25 @@ export const GhostwriterPanel: React.FC = ({ job }) => { setIsStreaming(false); } }, - [isStreaming, job.id, loadMessages, messages, onStreamEvent], + [isStreaming, job.id, loadThreadMessages, onStreamEvent], ); const stopStreaming = useCallback(async () => { - streamAbortRef.current?.abort(); - streamAbortRef.current = null; - setIsStreaming(false); - setStreamingMessageId(null); - const runId = activeRunId; - setActiveRunId(null); - - if (!runId) { - return; - } - + if (!activeThreadId || !activeRunId) return; try { - await api.cancelJobGhostwriterRun(job.id, runId); - await loadMessages(); + await api.cancelJobChatRun(job.id, activeThreadId, activeRunId); + streamAbortRef.current?.abort(); + streamAbortRef.current = null; + setIsStreaming(false); + setActiveRunId(null); + setStreamingMessageId(null); + await loadThreadMessages(activeThreadId); } catch (error) { const message = error instanceof Error ? error.message : "Failed to stop run"; toast.error(message); } - }, [activeRunId, job.id, loadMessages]); + }, [activeThreadId, activeRunId, job.id, loadThreadMessages]); const canRegenerate = useMemo(() => { if (isStreaming || messages.length === 0) return false; @@ -211,7 +247,7 @@ export const GhostwriterPanel: React.FC = ({ job }) => { }, [isStreaming, messages]); const regenerate = useCallback(async () => { - if (isStreaming || messages.length === 0) return; + if (!activeThreadId || isStreaming || messages.length === 0) return; const last = messages[messages.length - 1]; if (last.role !== "assistant") return; @@ -220,13 +256,14 @@ export const GhostwriterPanel: React.FC = ({ job }) => { streamAbortRef.current = controller; try { - await api.streamRegenerateJobGhostwriterMessage( + await api.streamRegenerateJobChatMessage( job.id, + activeThreadId, last.id, { signal: controller.signal }, { onEvent: onStreamEvent }, ); - await loadMessages(); + await loadThreadMessages(activeThreadId); } catch (error) { if (error instanceof Error && error.name === "AbortError") { return; @@ -240,44 +277,60 @@ export const GhostwriterPanel: React.FC = ({ job }) => { streamAbortRef.current = null; setIsStreaming(false); } - }, [isStreaming, job.id, loadMessages, messages, onStreamEvent]); + }, [ + activeThreadId, + isStreaming, + job.id, + loadThreadMessages, + messages, + onStreamEvent, + ]); return ( -
-
- {messages.length === 0 && !isLoading ? ( -
-

- {job.title} at {job.employer} -

-

- Ghostwriter already has this job description, your resume and your - writing style preferences. Ask for tailored response drafts, or - concise role-fit talking points. -

-
- ) : ( + + + + + Ghostwriter + + + + { + setActiveThreadId(threadId); + void loadThreadMessages(threadId); + }} + onCreateThread={() => { + void createThread(); + }} + disabled={isLoading || isStreaming} + /> + +
- )} -
+
-
- + + -
-
+ + ); }; diff --git a/orchestrator/src/client/components/ghostwriter/MessageList.tsx b/orchestrator/src/client/components/ghostwriter/MessageList.tsx index 297d4de..5c4e29c 100644 --- a/orchestrator/src/client/components/ghostwriter/MessageList.tsx +++ b/orchestrator/src/client/components/ghostwriter/MessageList.tsx @@ -1,7 +1,5 @@ import type { JobChatMessage } from "@shared/types"; import type React from "react"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; import { StreamingMessage } from "./StreamingMessage"; type MessageListProps = { @@ -17,7 +15,12 @@ export const MessageList: React.FC = ({ }) => { return (
- {messages.length > 0 && + {messages.length === 0 ? ( +
+ Ask for interview prep, response drafts, or application strategy for + this job. +
+ ) : ( messages.map((message) => { const isUser = message.role === "user"; const isActiveStreaming = @@ -37,24 +40,20 @@ export const MessageList: React.FC = ({
{isUser ? "You" - : `Ghostwriter${message.version > 1 ? ` v${message.version}` : ""}`} + : `Copilot${message.version > 1 ? ` v${message.version}` : ""}`}
{isActiveStreaming ? ( - ) : message.role === "assistant" ? ( -
- - {message.content || "..."} - -
) : (
- {message.content} + {message.content || + (message.role === "assistant" ? "..." : "")}
)}
); - })} + }) + )} ); }; diff --git a/orchestrator/src/client/components/ghostwriter/ThreadList.tsx b/orchestrator/src/client/components/ghostwriter/ThreadList.tsx index 7c07b07..07a7414 100644 --- a/orchestrator/src/client/components/ghostwriter/ThreadList.tsx +++ b/orchestrator/src/client/components/ghostwriter/ThreadList.tsx @@ -1,72 +1,27 @@ -import type { Job, JobChatThread } from "@shared/types"; +import type { 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 magnitudeMinutes = Math.max(1, Math.round(absMs / minute)); - const minutes = Math.sign(diffMs) * magnitudeMinutes; - 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 ( - + ); }; diff --git a/orchestrator/src/client/components/job-chat/Composer.tsx b/orchestrator/src/client/components/job-chat/Composer.tsx deleted file mode 100644 index 167744d..0000000 --- a/orchestrator/src/client/components/job-chat/Composer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Send } from "lucide-react"; -import type React from "react"; -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; - -type ComposerProps = { - disabled?: boolean; - onSend: (content: string) => Promise; -}; - -export const Composer: React.FC = ({ disabled, onSend }) => { - const [value, setValue] = useState(""); - - const submit = async () => { - const content = value.trim(); - if (!content || disabled) return; - setValue(""); - await onSend(content); - }; - - return ( -
-