@aprovan/patchwork-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +16 -0
- package/LICENSE +373 -0
- package/dist/index.d.ts +331 -0
- package/dist/index.js +1597 -0
- package/package.json +45 -0
- package/src/components/CodeBlockExtension.tsx +190 -0
- package/src/components/CodePreview.tsx +344 -0
- package/src/components/MarkdownEditor.tsx +270 -0
- package/src/components/ServicesInspector.tsx +118 -0
- package/src/components/edit/EditHistory.tsx +89 -0
- package/src/components/edit/EditModal.tsx +236 -0
- package/src/components/edit/FileTree.tsx +144 -0
- package/src/components/edit/api.ts +100 -0
- package/src/components/edit/index.ts +6 -0
- package/src/components/edit/types.ts +53 -0
- package/src/components/edit/useEditSession.ts +164 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +72 -0
- package/src/lib/code-extractor.ts +210 -0
- package/src/lib/diff.ts +308 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vfs.ts +106 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
2
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
3
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
4
|
+
import Typography from '@tiptap/extension-typography';
|
|
5
|
+
import { Markdown } from 'tiptap-markdown';
|
|
6
|
+
import { TextSelection } from '@tiptap/pm/state';
|
|
7
|
+
import { useEffect, useCallback, useRef } from 'react';
|
|
8
|
+
import { CodeBlockExtension } from './CodeBlockExtension';
|
|
9
|
+
|
|
10
|
+
interface MarkdownEditorProps {
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
onSubmit: () => void;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function MarkdownEditor({
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
onSubmit,
|
|
23
|
+
placeholder = 'Type a message...',
|
|
24
|
+
disabled = false,
|
|
25
|
+
className = '',
|
|
26
|
+
}: MarkdownEditorProps) {
|
|
27
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
const editor = useEditor({
|
|
30
|
+
extensions: [
|
|
31
|
+
StarterKit.configure({
|
|
32
|
+
heading: { levels: [1, 2, 3] },
|
|
33
|
+
bulletList: { keepMarks: true, keepAttributes: false },
|
|
34
|
+
orderedList: { keepMarks: true, keepAttributes: false },
|
|
35
|
+
codeBlock: false, // Use our custom CodeBlockExtension
|
|
36
|
+
code: {
|
|
37
|
+
HTMLAttributes: {
|
|
38
|
+
class: 'bg-muted rounded px-1 py-0.5 font-mono text-sm',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
blockquote: {
|
|
42
|
+
HTMLAttributes: {
|
|
43
|
+
class: 'border-l-4 border-muted-foreground/30 pl-4 italic',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
horizontalRule: false,
|
|
47
|
+
hardBreak: { keepMarks: false },
|
|
48
|
+
}),
|
|
49
|
+
CodeBlockExtension,
|
|
50
|
+
Placeholder.configure({
|
|
51
|
+
placeholder,
|
|
52
|
+
emptyEditorClass: 'is-editor-empty',
|
|
53
|
+
}),
|
|
54
|
+
Typography,
|
|
55
|
+
Markdown.configure({
|
|
56
|
+
html: false,
|
|
57
|
+
transformPastedText: true,
|
|
58
|
+
transformCopiedText: true,
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
content: value,
|
|
62
|
+
editable: !disabled,
|
|
63
|
+
editorProps: {
|
|
64
|
+
attributes: {
|
|
65
|
+
class: `outline-none min-h-[40px] max-h-[200px] overflow-y-auto px-3 py-2 ${className}`,
|
|
66
|
+
},
|
|
67
|
+
handleKeyDown: (view, event) => {
|
|
68
|
+
const { state } = view;
|
|
69
|
+
const { selection } = state;
|
|
70
|
+
const { $from, $to, empty } = selection;
|
|
71
|
+
|
|
72
|
+
const parentType = $from.parent.type.name;
|
|
73
|
+
const isInList = parentType === 'listItem';
|
|
74
|
+
const isInCodeBlock = parentType === 'codeBlock';
|
|
75
|
+
|
|
76
|
+
// Cmd/Ctrl+A in code block: select just the code block content
|
|
77
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'a' && isInCodeBlock) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
const { tr } = state;
|
|
80
|
+
tr.setSelection(TextSelection.create(tr.doc, $from.start(), $from.end()));
|
|
81
|
+
view.dispatch(tr);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ArrowUp at first line of code block: focus language input
|
|
86
|
+
if (event.key === 'ArrowUp' && isInCodeBlock) {
|
|
87
|
+
const textBefore = $from.parent.textContent.slice(0, $from.parentOffset);
|
|
88
|
+
if (!textBefore.includes('\n')) {
|
|
89
|
+
const domPos = view.domAtPos($from.start());
|
|
90
|
+
const wrapper = domPos.node.parentElement?.closest('.code-block-wrapper');
|
|
91
|
+
const langInput = wrapper?.querySelector('.language-input') as HTMLInputElement;
|
|
92
|
+
if (langInput) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
langInput.focus();
|
|
95
|
+
langInput.select();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Tab handling in code blocks
|
|
102
|
+
if (event.key === 'Tab' && isInCodeBlock) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
const { tr } = state;
|
|
105
|
+
|
|
106
|
+
if (empty) {
|
|
107
|
+
if (event.shiftKey) {
|
|
108
|
+
const textBefore = $from.parent.textContent.slice(0, $from.parentOffset);
|
|
109
|
+
if (textBefore.endsWith(' ')) {
|
|
110
|
+
tr.delete($from.pos - 2, $from.pos);
|
|
111
|
+
} else if (textBefore.endsWith(' ')) {
|
|
112
|
+
tr.delete($from.pos - 1, $from.pos);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
tr.insertText(' ');
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const text = $from.parent.textContent;
|
|
119
|
+
const lineStart = text.lastIndexOf('\n', $from.parentOffset - 1) + 1;
|
|
120
|
+
const lineEnd = text.indexOf('\n', $to.parentOffset);
|
|
121
|
+
const actualEnd = lineEnd === -1 ? text.length : lineEnd;
|
|
122
|
+
const selectedText = text.slice(lineStart, actualEnd);
|
|
123
|
+
const lines = selectedText.split('\n');
|
|
124
|
+
|
|
125
|
+
const newText = event.shiftKey
|
|
126
|
+
? lines.map(line => line.replace(/^ ?/, '')).join('\n')
|
|
127
|
+
: lines.map(line => ' ' + line).join('\n');
|
|
128
|
+
|
|
129
|
+
const blockStart = $from.start();
|
|
130
|
+
tr.replaceWith(blockStart + lineStart, blockStart + actualEnd, state.schema.text(newText));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
view.dispatch(tr);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Enter key handling
|
|
138
|
+
if (event.key === 'Enter') {
|
|
139
|
+
if (isInCodeBlock) {
|
|
140
|
+
if (event.shiftKey) {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
const { tr } = state;
|
|
143
|
+
tr.insertText('\n');
|
|
144
|
+
view.dispatch(tr);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
onSubmit();
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Shift+Enter: create new paragraph (enables input rules on new lines)
|
|
153
|
+
if (event.shiftKey) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
const { tr } = state;
|
|
156
|
+
view.dispatch(tr.split($from.pos));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Regular Enter: submit (unless in list)
|
|
161
|
+
if (!isInList) {
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
onSubmit();
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return false;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
onUpdate: ({ editor }) => {
|
|
172
|
+
// Get markdown output from tiptap-markdown extension
|
|
173
|
+
const markdownStorage = (editor.storage as any).markdown;
|
|
174
|
+
if (markdownStorage?.getMarkdown) {
|
|
175
|
+
onChange(markdownStorage.getMarkdown());
|
|
176
|
+
} else {
|
|
177
|
+
onChange(editor.getText());
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Handle paste - prefer plain text to avoid VS Code HTML wrapping
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!editor) return;
|
|
185
|
+
|
|
186
|
+
const handlePaste = (event: ClipboardEvent) => {
|
|
187
|
+
const plainText = event.clipboardData?.getData('text/plain');
|
|
188
|
+
const htmlText = event.clipboardData?.getData('text/html');
|
|
189
|
+
|
|
190
|
+
if (!plainText) return;
|
|
191
|
+
|
|
192
|
+
const { $from } = editor.state.selection;
|
|
193
|
+
const isInCodeBlock = $from.parent.type.name === 'codeBlock';
|
|
194
|
+
|
|
195
|
+
// In code blocks, always paste as plain text
|
|
196
|
+
if (isInCodeBlock) {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
const { tr } = editor.state;
|
|
200
|
+
tr.insertText(plainText);
|
|
201
|
+
editor.view.dispatch(tr);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Outside code blocks: intercept HTML to parse as markdown
|
|
206
|
+
if (htmlText) {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
|
|
210
|
+
const markdownStorage = (editor.storage as any).markdown;
|
|
211
|
+
if (markdownStorage?.parser) {
|
|
212
|
+
try {
|
|
213
|
+
const parsed = markdownStorage.parser.parse(plainText);
|
|
214
|
+
if (parsed?.content?.size > 0) {
|
|
215
|
+
const nodes: any[] = [];
|
|
216
|
+
parsed.content.forEach((node: any) => nodes.push(node.toJSON()));
|
|
217
|
+
editor.chain().focus().insertContent(nodes).run();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.warn('Markdown parse failed, falling back to plain text', e);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
editor.chain().focus().insertContent(plainText).run();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const container = containerRef.current;
|
|
230
|
+
container?.addEventListener('paste', handlePaste, { capture: true });
|
|
231
|
+
return () => container?.removeEventListener('paste', handlePaste, { capture: true });
|
|
232
|
+
}, [editor]);
|
|
233
|
+
|
|
234
|
+
// Clear content when value is reset externally
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (editor && value === '' && editor.getText() !== '') {
|
|
237
|
+
editor.commands.clearContent();
|
|
238
|
+
}
|
|
239
|
+
}, [editor, value]);
|
|
240
|
+
|
|
241
|
+
// Update editable state
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
editor?.setEditable(!disabled);
|
|
244
|
+
}, [editor, disabled]);
|
|
245
|
+
|
|
246
|
+
const focus = useCallback(() => {
|
|
247
|
+
editor?.commands.focus();
|
|
248
|
+
}, [editor]);
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (editor) {
|
|
252
|
+
(editor as any).focusInput = focus;
|
|
253
|
+
}
|
|
254
|
+
}, [editor, focus]);
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div
|
|
258
|
+
ref={containerRef}
|
|
259
|
+
className={`
|
|
260
|
+
flex-1 rounded-md border border-input bg-background text-sm
|
|
261
|
+
ring-offset-background
|
|
262
|
+
focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2
|
|
263
|
+
${disabled ? 'cursor-not-allowed opacity-50' : ''}
|
|
264
|
+
`}
|
|
265
|
+
onClick={() => editor?.commands.focus()}
|
|
266
|
+
>
|
|
267
|
+
<EditorContent editor={editor} className="markdown-editor" />
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { ChevronDown, Server } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface ServiceInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
namespace: string;
|
|
7
|
+
procedure: string;
|
|
8
|
+
description: string;
|
|
9
|
+
parameters: {
|
|
10
|
+
jsonSchema: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ServicesInspectorProps {
|
|
15
|
+
namespaces: string[];
|
|
16
|
+
services?: ServiceInfo[];
|
|
17
|
+
/** Custom badge component for rendering the service count */
|
|
18
|
+
BadgeComponent?: React.ComponentType<{ children: React.ReactNode; variant?: string; className?: string }>;
|
|
19
|
+
/** Custom collapsible components */
|
|
20
|
+
CollapsibleComponent?: React.ComponentType<{ children: React.ReactNode; defaultOpen?: boolean; className?: string }>;
|
|
21
|
+
CollapsibleTriggerComponent?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
|
|
22
|
+
CollapsibleContentComponent?: React.ComponentType<{ children: React.ReactNode }>;
|
|
23
|
+
/** Custom dialog components */
|
|
24
|
+
DialogComponent?: React.ComponentType<{ children: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }>;
|
|
25
|
+
DialogHeaderComponent?: React.ComponentType<{ children: React.ReactNode }>;
|
|
26
|
+
DialogContentComponent?: React.ComponentType<{ children: React.ReactNode }>;
|
|
27
|
+
DialogCloseComponent?: React.ComponentType<{ onClose?: () => void }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback components for when custom UI is not provided
|
|
31
|
+
function DefaultBadge({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
|
32
|
+
return (
|
|
33
|
+
<span className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`}>
|
|
34
|
+
{children}
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function DefaultDialog({ children, open, onOpenChange }: { children: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }) {
|
|
40
|
+
if (!open) return null;
|
|
41
|
+
return (
|
|
42
|
+
<div className="fixed inset-0 z-50 bg-black/50" onClick={() => onOpenChange?.(false)}>
|
|
43
|
+
<div className="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg" onClick={e => e.stopPropagation()}>
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ServicesInspector({
|
|
51
|
+
namespaces,
|
|
52
|
+
services = [],
|
|
53
|
+
BadgeComponent = DefaultBadge,
|
|
54
|
+
DialogComponent = DefaultDialog,
|
|
55
|
+
}: ServicesInspectorProps) {
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
|
|
58
|
+
if (namespaces.length === 0) return null;
|
|
59
|
+
|
|
60
|
+
const groupedServices = services.reduce<Record<string, ServiceInfo[]>>((acc, svc) => {
|
|
61
|
+
(acc[svc.namespace] ??= []).push(svc);
|
|
62
|
+
return acc;
|
|
63
|
+
}, {});
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => setOpen(true)}
|
|
69
|
+
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
|
70
|
+
>
|
|
71
|
+
<Server className="h-4 w-4 text-muted-foreground" />
|
|
72
|
+
<BadgeComponent className="text-xs">
|
|
73
|
+
{namespaces.length} service{namespaces.length !== 1 ? 's' : ''}
|
|
74
|
+
</BadgeComponent>
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
<DialogComponent open={open} onOpenChange={setOpen}>
|
|
78
|
+
<div className="flex justify-between items-center mb-4">
|
|
79
|
+
<h2 className="text-lg font-semibold">Available Services</h2>
|
|
80
|
+
<button onClick={() => setOpen(false)} className="text-muted-foreground hover:text-foreground">×</button>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="space-y-3 max-h-96 overflow-auto">
|
|
83
|
+
{namespaces.map((ns) => (
|
|
84
|
+
<details key={ns} open={namespaces.length === 1}>
|
|
85
|
+
<summary className="flex items-center gap-2 w-full p-2 rounded bg-muted/50 hover:bg-muted transition-colors cursor-pointer">
|
|
86
|
+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
87
|
+
<span className="font-medium text-sm">{ns}</span>
|
|
88
|
+
{groupedServices[ns] && (
|
|
89
|
+
<BadgeComponent className="ml-auto text-xs">
|
|
90
|
+
{groupedServices[ns].length} tool{groupedServices[ns].length !== 1 ? 's' : ''}
|
|
91
|
+
</BadgeComponent>
|
|
92
|
+
)}
|
|
93
|
+
</summary>
|
|
94
|
+
<div className="ml-6 mt-2 space-y-2">
|
|
95
|
+
{groupedServices[ns]?.map((svc) => (
|
|
96
|
+
<details key={svc.name}>
|
|
97
|
+
<summary className="flex items-center gap-2 w-full text-left text-sm hover:text-foreground text-muted-foreground transition-colors cursor-pointer">
|
|
98
|
+
<ChevronDown className="h-3 w-3" />
|
|
99
|
+
<code className="font-mono text-xs">{svc.procedure}</code>
|
|
100
|
+
<span className="truncate text-xs opacity-70">{svc.description}</span>
|
|
101
|
+
</summary>
|
|
102
|
+
<div className="ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48">
|
|
103
|
+
<pre className="text-xs font-mono whitespace-pre-wrap break-words m-0">
|
|
104
|
+
{JSON.stringify(svc.parameters.jsonSchema, null, 2)}
|
|
105
|
+
</pre>
|
|
106
|
+
</div>
|
|
107
|
+
</details>
|
|
108
|
+
)) ?? (
|
|
109
|
+
<p className="text-xs text-muted-foreground">No tool details available</p>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</details>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</DialogComponent>
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
import { Loader2 } from 'lucide-react';
|
|
3
|
+
import Markdown from 'react-markdown';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import type { EditHistoryEntry } from './types';
|
|
6
|
+
|
|
7
|
+
interface EditHistoryProps {
|
|
8
|
+
entries: EditHistoryEntry[];
|
|
9
|
+
streamingNotes: string[];
|
|
10
|
+
isStreaming: boolean;
|
|
11
|
+
pendingPrompt?: string | null;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ProgressNote({ text, isLatest }: { text: string; isLatest?: boolean }) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex items-center gap-2 text-muted-foreground/60">
|
|
18
|
+
{isLatest && <Loader2 className="h-3 w-3 animate-spin" />}
|
|
19
|
+
<p className="text-xs italic">{text}</p>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function EditHistory({
|
|
25
|
+
entries,
|
|
26
|
+
streamingNotes,
|
|
27
|
+
isStreaming,
|
|
28
|
+
pendingPrompt,
|
|
29
|
+
className = '',
|
|
30
|
+
}: EditHistoryProps) {
|
|
31
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (scrollRef.current) {
|
|
35
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
36
|
+
}
|
|
37
|
+
}, [entries, streamingNotes, pendingPrompt]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
ref={scrollRef}
|
|
42
|
+
className={`overflow-y-auto p-4 space-y-4 bg-background ${className}`}
|
|
43
|
+
>
|
|
44
|
+
{entries.map((entry, i) => (
|
|
45
|
+
<div key={i} className="space-y-3">
|
|
46
|
+
<div className="flex justify-end">
|
|
47
|
+
<div className="bg-primary text-primary-foreground rounded-lg px-4 py-2 max-w-[85%]">
|
|
48
|
+
<div className="prose prose-sm prose-invert max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0">
|
|
49
|
+
<Markdown remarkPlugins={[remarkGfm]}>{entry.prompt}</Markdown>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="flex justify-start">
|
|
55
|
+
<div className="bg-primary/10 rounded-lg px-4 py-2 max-w-[85%]">
|
|
56
|
+
<div className="prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0">
|
|
57
|
+
<Markdown remarkPlugins={[remarkGfm]}>{entry.summary}</Markdown>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
|
|
64
|
+
{pendingPrompt && (
|
|
65
|
+
<div className="space-y-3">
|
|
66
|
+
<div className="flex justify-end">
|
|
67
|
+
<div className="bg-primary text-primary-foreground rounded-lg px-4 py-2 max-w-[85%]">
|
|
68
|
+
<div className="prose prose-sm prose-invert max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0">
|
|
69
|
+
<Markdown remarkPlugins={[remarkGfm]}>{pendingPrompt}</Markdown>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{isStreaming && streamingNotes.length > 0 && (
|
|
75
|
+
<div className="space-y-1 py-2 px-3">
|
|
76
|
+
{streamingNotes.map((note, i) => (
|
|
77
|
+
<ProgressNote
|
|
78
|
+
key={i}
|
|
79
|
+
text={note}
|
|
80
|
+
isLatest={i === streamingNotes.length - 1}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|