Skip to content

Commit 9f0e661

Browse files
authored
Turn prompt editor into a split workbench (#5005)
* refactor: turn prompt editor into a split workbench * save * template command * wip * fixed sidebar * fmt * only enable in dev
1 parent 0fd17b5 commit 9f0e661

45 files changed

Lines changed: 1654 additions & 695 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import type { ChatStatus } from "ai";
2+
import { tool } from "ai";
3+
import { ChevronDownIcon, SparklesIcon, WandSparklesIcon } from "lucide-react";
4+
import { useCallback, useMemo } from "react";
5+
import { z } from "zod";
6+
7+
import { Button } from "@hypr/ui/components/ui/button";
8+
import { cn } from "@hypr/utils";
9+
10+
import { useLanguageModel } from "~/ai/hooks";
11+
import type { TaskType } from "~/ai/prompts/config";
12+
import { ChatBodyNonEmpty } from "~/chat/components/body/non-empty";
13+
import { useChatAutoScroll } from "~/chat/components/body/use-chat-auto-scroll";
14+
import { ChatMessageInput } from "~/chat/components/input";
15+
import { ChatSession } from "~/chat/components/session-provider";
16+
import type { HyprUIMessage } from "~/chat/types";
17+
import { id } from "~/shared/utils";
18+
19+
const PROMPT_ASSISTANT_SUGGESTIONS = {
20+
enhance: [
21+
"Make this prompt more action-item focused.",
22+
"Tighten the structure so the instructions feel shorter and clearer.",
23+
"Rewrite this so the output reads more executive and less verbose.",
24+
],
25+
title: [
26+
"Make the title guidance punchier and less generic.",
27+
"Bias the title toward decisions instead of broad meeting names.",
28+
"Shorten the title output to four or five words max.",
29+
],
30+
} satisfies Record<TaskType, string[]>;
31+
32+
export function PromptAssistantPanel({
33+
selectedTask,
34+
taskLabel,
35+
taskDescription,
36+
variables,
37+
filters,
38+
draftContent,
39+
hasCustomPrompt,
40+
onApplyTemplate,
41+
}: {
42+
selectedTask: TaskType;
43+
taskLabel: string;
44+
taskDescription: string;
45+
variables: string[];
46+
filters: string[];
47+
draftContent: string;
48+
hasCustomPrompt: boolean;
49+
onApplyTemplate: (content: string) => void;
50+
}) {
51+
const model = useLanguageModel("chat");
52+
const sessionId = useMemo(() => id(), [selectedTask]);
53+
54+
const assistantPrompt = useMemo(
55+
() =>
56+
[
57+
"You are helping the user edit a custom Jinja template for Char.",
58+
`Task: ${taskLabel}`,
59+
`Description: ${taskDescription}`,
60+
"",
61+
"This editor controls the custom override surface rendered with renderCustom(...).",
62+
"Do not refer to internal Askama macros or hidden template helpers.",
63+
"",
64+
`Saved state: ${hasCustomPrompt ? "custom override" : "default behavior with no override saved yet"}`,
65+
`Available variables: ${variables.join(", ") || "none"}`,
66+
`Available filters: ${filters.join(", ") || "none"}`,
67+
"",
68+
"Rules:",
69+
"- Keep responses concise.",
70+
"- Explain changes briefly before or after applying them.",
71+
"- When the user asks for a concrete change, call update_prompt_template with the full next template.",
72+
"- Do not say the draft was updated unless you actually call the tool.",
73+
"- Preserve valid Jinja syntax.",
74+
"",
75+
"<current_template>",
76+
draftContent,
77+
"</current_template>",
78+
].join("\n"),
79+
[
80+
draftContent,
81+
filters,
82+
hasCustomPrompt,
83+
taskDescription,
84+
taskLabel,
85+
variables,
86+
],
87+
);
88+
89+
const extraTools = useMemo(
90+
() => ({
91+
update_prompt_template: tool({
92+
description:
93+
"Replace the current prompt draft with a complete updated Jinja template.",
94+
inputSchema: z.object({
95+
content: z
96+
.string()
97+
.describe("The full updated prompt template in Jinja syntax."),
98+
summary: z
99+
.string()
100+
.optional()
101+
.describe("A short note about what changed in the draft."),
102+
}),
103+
execute: async ({
104+
content,
105+
summary,
106+
}: {
107+
content: string;
108+
summary?: string;
109+
}) => {
110+
const nextContent = content.trim();
111+
onApplyTemplate(nextContent);
112+
113+
return {
114+
status: "applied",
115+
message:
116+
summary ??
117+
"Draft updated in the editor. Review and save to make it live.",
118+
lineCount: nextContent.split("\n").length,
119+
};
120+
},
121+
}),
122+
}),
123+
[onApplyTemplate],
124+
);
125+
126+
const handleSendMessage = useCallback(
127+
(
128+
_content: string,
129+
parts: HyprUIMessage["parts"],
130+
sendMessage: (message: HyprUIMessage) => void,
131+
) => {
132+
sendMessage({
133+
id: id(),
134+
role: "user",
135+
parts,
136+
metadata: {
137+
createdAt: Date.now(),
138+
},
139+
});
140+
},
141+
[],
142+
);
143+
144+
return (
145+
<div className="flex h-full min-h-0 flex-col bg-stone-50">
146+
<div className="border-b border-neutral-200 px-4 py-4">
147+
<div className="flex items-center gap-2 text-sm font-medium text-neutral-900">
148+
<WandSparklesIcon className="h-4 w-4 text-neutral-500" />
149+
Prompt Assistant
150+
</div>
151+
<p className="mt-1 text-xs leading-5 text-neutral-600">
152+
Ask Charlie to rewrite the draft, tighten the language, or reshape the
153+
structure. Applied changes land back in the editor so you can review
154+
them before saving.
155+
</p>
156+
</div>
157+
158+
<ChatSession
159+
key={selectedTask}
160+
sessionId={sessionId}
161+
modelOverride={model ?? undefined}
162+
extraTools={extraTools}
163+
systemPromptOverride={assistantPrompt}
164+
>
165+
{(sessionProps) => (
166+
<div className="flex min-h-0 flex-1 flex-col">
167+
<PromptAssistantBody
168+
messages={sessionProps.messages}
169+
status={sessionProps.status}
170+
error={sessionProps.error}
171+
regenerate={sessionProps.regenerate}
172+
isModelConfigured={!!model}
173+
selectedTask={selectedTask}
174+
onSendPrompt={(prompt) => {
175+
handleSendMessage(
176+
prompt,
177+
[{ type: "text", text: prompt }],
178+
sessionProps.sendMessage,
179+
);
180+
}}
181+
/>
182+
183+
{model ? (
184+
<ChatMessageInput
185+
draftKey={sessionProps.sessionId}
186+
disabled={!sessionProps.isSystemPromptReady}
187+
onSendMessage={(content, parts) => {
188+
handleSendMessage(content, parts, sessionProps.sendMessage);
189+
}}
190+
isStreaming={
191+
sessionProps.status === "streaming" ||
192+
sessionProps.status === "submitted"
193+
}
194+
onStop={sessionProps.stop}
195+
/>
196+
) : (
197+
<div className="border-t border-neutral-200 px-4 py-3 text-xs text-neutral-500">
198+
Configure a chat model in AI settings to edit prompts from chat.
199+
</div>
200+
)}
201+
</div>
202+
)}
203+
</ChatSession>
204+
</div>
205+
);
206+
}
207+
208+
function PromptAssistantBody({
209+
messages,
210+
status,
211+
error,
212+
regenerate,
213+
isModelConfigured,
214+
selectedTask,
215+
onSendPrompt,
216+
}: {
217+
messages: HyprUIMessage[];
218+
status: ChatStatus;
219+
error?: Error;
220+
regenerate: () => void;
221+
isModelConfigured: boolean;
222+
selectedTask: TaskType;
223+
onSendPrompt: (prompt: string) => void;
224+
}) {
225+
const {
226+
contentRef,
227+
isAtBottom,
228+
scrollRef,
229+
scrollToBottom,
230+
showGoToRecent,
231+
updateAutoScrollState,
232+
handleWheel,
233+
} = useChatAutoScroll(status);
234+
235+
return (
236+
<div className="relative flex min-h-0 flex-1 flex-col">
237+
<div
238+
ref={scrollRef}
239+
onScroll={updateAutoScrollState}
240+
onWheel={handleWheel}
241+
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
242+
>
243+
<div
244+
ref={contentRef}
245+
className="flex min-h-full flex-1 flex-col px-3 py-3"
246+
>
247+
<div className="flex-1" />
248+
{messages.length === 0 ? (
249+
<PromptAssistantEmpty
250+
isModelConfigured={isModelConfigured}
251+
selectedTask={selectedTask}
252+
onSendPrompt={onSendPrompt}
253+
/>
254+
) : (
255+
<ChatBodyNonEmpty
256+
messages={messages}
257+
status={status}
258+
error={error}
259+
onReload={regenerate}
260+
/>
261+
)}
262+
</div>
263+
</div>
264+
265+
{messages.length > 0 && showGoToRecent && !isAtBottom ? (
266+
<Button
267+
onClick={scrollToBottom}
268+
size="sm"
269+
className="absolute bottom-3 left-1/2 z-20 flex -translate-x-1/2 items-center gap-1 rounded-full border border-neutral-200 bg-white text-neutral-700 shadow-xs hover:bg-neutral-50"
270+
variant="outline"
271+
>
272+
<ChevronDownIcon size={12} />
273+
<span className="text-xs">Go to recent</span>
274+
</Button>
275+
) : null}
276+
</div>
277+
);
278+
}
279+
280+
function PromptAssistantEmpty({
281+
isModelConfigured,
282+
selectedTask,
283+
onSendPrompt,
284+
}: {
285+
isModelConfigured: boolean;
286+
selectedTask: TaskType;
287+
onSendPrompt: (prompt: string) => void;
288+
}) {
289+
return (
290+
<div className="flex justify-start pb-1">
291+
<div className="flex w-full flex-col">
292+
<div className="mb-2 flex items-center gap-2">
293+
<SparklesIcon className="h-4 w-4 text-neutral-500" />
294+
<span className="text-sm font-medium text-neutral-800">Charlie</span>
295+
</div>
296+
<p className="mb-3 text-sm leading-6 text-neutral-700">
297+
{isModelConfigured
298+
? "I can rewrite the active draft, preserve the Jinja structure, and apply the updated template back into the editor."
299+
: "Set up a chat model to rewrite the active draft from this pane."}
300+
</p>
301+
{isModelConfigured ? (
302+
<div className="flex flex-wrap gap-1.5">
303+
{PROMPT_ASSISTANT_SUGGESTIONS[selectedTask].map((prompt) => (
304+
<button
305+
key={prompt}
306+
type="button"
307+
onClick={() => onSendPrompt(prompt)}
308+
className={cn([
309+
"rounded-full border border-neutral-300 bg-white px-2.5 py-1 text-left text-[11px] text-neutral-700",
310+
"transition-colors hover:bg-neutral-100",
311+
])}
312+
>
313+
{prompt}
314+
</button>
315+
))}
316+
</div>
317+
) : null}
318+
</div>
319+
</div>
320+
);
321+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type TaskType = "enhance" | "title";
2+
3+
export const AVAILABLE_FILTERS = ["transcript", "url"] as const;
4+
5+
export const TASK_CONFIGS = [
6+
{
7+
type: "enhance" as const,
8+
label: "Enhance Notes",
9+
description: "Generates structured meeting summaries from transcripts",
10+
variables: [
11+
"content",
12+
"session",
13+
"participants",
14+
"template",
15+
"pre_meeting_memo",
16+
"post_meeting_memo",
17+
],
18+
},
19+
{
20+
type: "title" as const,
21+
label: "Title Generation",
22+
description: "Generates a title for the meeting note",
23+
variables: ["enhanced_note"],
24+
},
25+
] as const;

0 commit comments

Comments
 (0)