larkup 0.1.14

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.
Files changed (141) hide show
  1. package/.env +2 -0
  2. package/app/actions/vercel.ts +417 -0
  3. package/app/api/chat/route.ts +192 -0
  4. package/app/api/chat/status/route.ts +55 -0
  5. package/app/api/chat/suggestions/route.ts +115 -0
  6. package/app/api/config/route.ts +73 -0
  7. package/app/api/config/test/route.ts +67 -0
  8. package/app/api/config/test-embedding/route.ts +40 -0
  9. package/app/api/config/test-llm/route.ts +47 -0
  10. package/app/api/config/test-provider/route.ts +36 -0
  11. package/app/api/demo/route.ts +176 -0
  12. package/app/api/deploy/ssh/route.ts +340 -0
  13. package/app/api/documents/route.ts +90 -0
  14. package/app/api/firecrawl/local/route.ts +46 -0
  15. package/app/api/index/route.ts +113 -0
  16. package/app/api/jobs/[id]/route.ts +44 -0
  17. package/app/api/jobs/route.ts +116 -0
  18. package/app/api/parse-file/route.ts +39 -0
  19. package/app/api/search/google/route.ts +162 -0
  20. package/app/api/search/route.ts +29 -0
  21. package/app/api/server/download/route.ts +112 -0
  22. package/app/api/server/generate/route.ts +46 -0
  23. package/app/api/server/local/route.ts +42 -0
  24. package/app/api/servers/route.ts +137 -0
  25. package/app/api/vector-stores/install/route.ts +65 -0
  26. package/app/api/vector-stores/status/route.ts +26 -0
  27. package/app/chat/page.tsx +15 -0
  28. package/app/configure/page.tsx +53 -0
  29. package/app/data/page.tsx +15 -0
  30. package/app/demo/page.tsx +15 -0
  31. package/app/favicon.ico +0 -0
  32. package/app/globals.css +190 -0
  33. package/app/index-data/page.tsx +15 -0
  34. package/app/layout.tsx +51 -0
  35. package/app/page.tsx +34 -0
  36. package/app/server/page.tsx +22 -0
  37. package/app/simple/chat/page.tsx +17 -0
  38. package/app/simple/docs/page.tsx +21 -0
  39. package/app/simple/layout.tsx +9 -0
  40. package/app/simple/settings/page.tsx +627 -0
  41. package/app/template.tsx +16 -0
  42. package/app/themes.css +733 -0
  43. package/components/alerts/generic-alert.tsx +74 -0
  44. package/components/app-sidebar.tsx +187 -0
  45. package/components/app-topnav.tsx +195 -0
  46. package/components/chat/chat-settings-modal.tsx +209 -0
  47. package/components/chat/chat-workspace.tsx +348 -0
  48. package/components/chat/knowledge-base-result.tsx +97 -0
  49. package/components/chat/message-item.tsx +103 -0
  50. package/components/client-layout-wrapper.tsx +182 -0
  51. package/components/configure/configure-form.tsx +1683 -0
  52. package/components/configure/store-fields.tsx +152 -0
  53. package/components/data/corpus-panel.tsx +499 -0
  54. package/components/data/data-workspace.tsx +272 -0
  55. package/components/data/firecrawl-notice.tsx +236 -0
  56. package/components/data/jobs-panel.tsx +485 -0
  57. package/components/data/paste-panel.tsx +79 -0
  58. package/components/data/scrape-panel.tsx +964 -0
  59. package/components/data/upload-panel.tsx +714 -0
  60. package/components/demo/demo-workspace.tsx +419 -0
  61. package/components/global-settings.tsx +286 -0
  62. package/components/index/index-workspace.tsx +482 -0
  63. package/components/onboarding/simple-setup.tsx +702 -0
  64. package/components/onboarding/tech-setup.tsx +121 -0
  65. package/components/onboarding/welcome-screen.tsx +183 -0
  66. package/components/page-header.tsx +44 -0
  67. package/components/server/code-viewer.tsx +87 -0
  68. package/components/server/deploy-button.tsx +1390 -0
  69. package/components/server/server-workspace.tsx +392 -0
  70. package/components/simple/sdk-connect-dialog.tsx +201 -0
  71. package/components/simple/simple-index-button.tsx +83 -0
  72. package/components/simple/simple-sidebar.tsx +161 -0
  73. package/components/theme-customizer-provider.tsx +171 -0
  74. package/components/theme-switcher.tsx +445 -0
  75. package/components/ui/alert-dialog.tsx +187 -0
  76. package/components/ui/alert.tsx +76 -0
  77. package/components/ui/badge.tsx +52 -0
  78. package/components/ui/button.tsx +59 -0
  79. package/components/ui/card.tsx +103 -0
  80. package/components/ui/checkbox.tsx +28 -0
  81. package/components/ui/collapsible.tsx +21 -0
  82. package/components/ui/dialog.tsx +159 -0
  83. package/components/ui/dropdown-menu.tsx +281 -0
  84. package/components/ui/input.tsx +22 -0
  85. package/components/ui/label.tsx +20 -0
  86. package/components/ui/popover.tsx +90 -0
  87. package/components/ui/progress.tsx +83 -0
  88. package/components/ui/provider-icon.tsx +94 -0
  89. package/components/ui/scroll-area.tsx +55 -0
  90. package/components/ui/select.tsx +248 -0
  91. package/components/ui/separator.tsx +25 -0
  92. package/components/ui/sheet.tsx +138 -0
  93. package/components/ui/skeleton.tsx +13 -0
  94. package/components/ui/sonner.tsx +63 -0
  95. package/components/ui/switch.tsx +32 -0
  96. package/components/ui/table.tsx +116 -0
  97. package/components/ui/tabs.tsx +84 -0
  98. package/components/ui/textarea.tsx +18 -0
  99. package/components/ui/tooltip.tsx +66 -0
  100. package/components/workspace/delete-server-dialog.tsx +71 -0
  101. package/components/workspace/server-form-dialog.tsx +103 -0
  102. package/components/workspace/server-switcher.tsx +163 -0
  103. package/components/workspace/workspace-provider.tsx +177 -0
  104. package/components/workspace/workspace-top-bar.tsx +23 -0
  105. package/components.json +21 -0
  106. package/lib/error-formatter.ts +15 -0
  107. package/lib/utils.ts +6 -0
  108. package/next-env.d.ts +6 -0
  109. package/next.config.mjs +18 -0
  110. package/package.json +65 -0
  111. package/postcss.config.mjs +8 -0
  112. package/public/docker.png +0 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/github.svg +3 -0
  115. package/public/hetzner.svg +1 -0
  116. package/public/icons/aws.svg +38 -0
  117. package/public/icons/azure.svg +1 -0
  118. package/public/icons/chroma.png +0 -0
  119. package/public/icons/cohere.svg +30 -0
  120. package/public/icons/digital-ocean.webp +0 -0
  121. package/public/icons/firecrawl2.png +0 -0
  122. package/public/icons/gcp.svg +1 -0
  123. package/public/icons/gemini.svg +1 -0
  124. package/public/icons/jina.svg +1 -0
  125. package/public/icons/lancedb2.png +0 -0
  126. package/public/icons/mistral.svg +469 -0
  127. package/public/icons/nomic.png +0 -0
  128. package/public/icons/openai.svg +1 -0
  129. package/public/icons/pgvector2.png +0 -0
  130. package/public/icons/pinecone.png +0 -0
  131. package/public/icons/qdrant.svg +35 -0
  132. package/public/icons/supabase.png +0 -0
  133. package/public/icons/vercel copy.svg +1 -0
  134. package/public/icons/vercel.svg +1 -0
  135. package/public/icons/voyage-light.png +0 -0
  136. package/public/icons/weaviate.webp +0 -0
  137. package/public/logo-light.png +0 -0
  138. package/public/logo.png +0 -0
  139. package/public/logo9.png +0 -0
  140. package/public/vercel.svg +1 -0
  141. package/tsconfig.json +33 -0
@@ -0,0 +1,348 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect } from "react";
4
+ import { useChat } from "@ai-sdk/react";
5
+ import { DefaultChatTransport } from "ai";
6
+ import {
7
+ ArrowUp,
8
+ SquarePen,
9
+ RotateCcw,
10
+ TriangleAlert,
11
+ Loader2,
12
+ ChevronDown,
13
+ MessageCircle,
14
+ } from "lucide-react";
15
+ import useSWR from "swr";
16
+ import { MessageItem } from "@/components/chat/message-item";
17
+ import { ChatSettingsModal } from "@/components/chat/chat-settings-modal";
18
+ import { useWorkspace } from "@/components/workspace/workspace-provider";
19
+ import { cn } from "@/lib/utils";
20
+ import { PROVIDER_META, ProviderIcon } from "@/components/ui/provider-icon";
21
+
22
+ const fetcher = (url: string) => fetch(url).then((r) => r.json());
23
+
24
+ interface ChatStatus {
25
+ ready: boolean;
26
+ indexed: boolean;
27
+ blockers: string[];
28
+ provider: string;
29
+ chatModelId: string;
30
+ availableModels: { id: string; label: string; provider: string }[];
31
+ suggestions: string[];
32
+ }
33
+
34
+ export function ChatWorkspace() {
35
+ const { activeServer } = useWorkspace();
36
+ const serverId = activeServer?.id ?? null;
37
+
38
+ const statusKey = serverId
39
+ ? `/api/chat/status?serverId=${encodeURIComponent(serverId)}`
40
+ : "/api/chat/status";
41
+ const { data: status, isLoading: statusLoading } = useSWR<ChatStatus>(
42
+ statusKey,
43
+ fetcher,
44
+ { refreshInterval: 10000 },
45
+ );
46
+
47
+ const [selectedModel, setSelectedModel] = useState<string>("");
48
+ const [showModelSelect, setShowModelSelect] = useState(false);
49
+ const [input, setInput] = useState("");
50
+
51
+ // Update selected model when status loads
52
+ useEffect(() => {
53
+ if (status?.chatModelId && !selectedModel) {
54
+ setSelectedModel(status.chatModelId);
55
+ }
56
+ }, [status?.chatModelId, selectedModel]);
57
+
58
+ // Fetch suggestions if missing
59
+ useEffect(() => {
60
+ if (
61
+ status?.ready &&
62
+ status.suggestions &&
63
+ status.suggestions.length === 0
64
+ ) {
65
+ const url = serverId
66
+ ? `/api/chat/suggestions?serverId=${encodeURIComponent(serverId)}`
67
+ : "/api/chat/suggestions";
68
+ fetch(url, { method: "POST" });
69
+ }
70
+ }, [status?.ready, status?.suggestions, serverId]);
71
+
72
+ const {
73
+ messages,
74
+ sendMessage,
75
+ status: chatStatus,
76
+ setMessages,
77
+ error,
78
+ regenerate,
79
+ } = useChat({
80
+ transport: new DefaultChatTransport({
81
+ api: "/api/chat",
82
+ body: {
83
+ serverId,
84
+ chatModelId: selectedModel || undefined,
85
+ },
86
+ }),
87
+ });
88
+
89
+ const scrollRef = useRef<HTMLDivElement>(null);
90
+ const isBusy = chatStatus === "submitted" || chatStatus === "streaming";
91
+
92
+ useEffect(() => {
93
+ scrollRef.current?.scrollTo({
94
+ top: scrollRef.current.scrollHeight,
95
+ behavior: "smooth",
96
+ });
97
+ }, [messages, chatStatus]);
98
+
99
+ function handleSubmit(e: React.FormEvent) {
100
+ e.preventDefault();
101
+ const text = input.trim();
102
+ if (!text || isBusy) return;
103
+ sendMessage({ text });
104
+ setInput("");
105
+ }
106
+
107
+ function newChat() {
108
+ setMessages([]);
109
+ setInput("");
110
+ }
111
+
112
+ const isEmpty = messages.length === 0;
113
+ const ready = status?.ready ?? false;
114
+
115
+ if (statusLoading) {
116
+ return (
117
+ <div className="flex flex-1 items-center justify-center">
118
+ <Loader2 className="size-6 animate-spin text-muted-foreground" />
119
+ </div>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <div className="flex flex-1 flex-col overflow-hidden">
125
+ {/* Header bar */}
126
+ <div className="flex items-center justify-between border-b border-border px-6 pb-3">
127
+ <div className="flex items-center gap-3">
128
+ {/* We moved model selector to the right */}
129
+ </div>
130
+ <div className="flex items-center gap-2">
131
+ {/* Model selector */}
132
+ {status?.availableModels && status.availableModels.length > 0 && (
133
+ <div className="relative">
134
+ <button
135
+ type="button"
136
+ onClick={() => setShowModelSelect((o) => !o)}
137
+ className="flex w-[250px] justify-between items-center gap-1.5 rounded-lg border border-border bg-white px-3 py-1.5 text-xs text-foreground transition hover:bg-muted"
138
+ >
139
+ {status.availableModels.find((m) => m.id === selectedModel)
140
+ ?.label ||
141
+ selectedModel ||
142
+ "Select model"}
143
+ <ChevronDown className="size-3 text-muted-foreground" />
144
+ </button>
145
+ {showModelSelect && (
146
+ <>
147
+ <div
148
+ className="fixed inset-0 z-40"
149
+ onClick={() => setShowModelSelect(false)}
150
+ />
151
+ <div className="absolute right-0 top-full z-50 mt-1 max-h-64 min-w-[250px] overflow-y-auto rounded-lg border border-border bg-card p-1 shadow-lg [&::-webkit-scrollbar]:hidden">
152
+ {status.availableModels.map((m) => (
153
+ <button
154
+ key={m.id}
155
+ type="button"
156
+ onClick={() => {
157
+ setSelectedModel(m.id);
158
+ setShowModelSelect(false);
159
+ }}
160
+ className={cn(
161
+ "flex w-full gap-3 items-center justify-between rounded-md px-3 py-2 text-left text-xs transition",
162
+ m.id === selectedModel
163
+ ? "bg-primary/10 text-primary font-medium"
164
+ : "text-foreground hover:bg-muted",
165
+ )}
166
+ >
167
+ <span>{m.label}</span>
168
+ {PROVIDER_META[
169
+ m.provider as keyof typeof PROVIDER_META
170
+ ] ? (
171
+ <ProviderIcon
172
+ src={
173
+ PROVIDER_META[
174
+ m.provider as keyof typeof PROVIDER_META
175
+ ].iconSrc
176
+ }
177
+ alt={m.provider}
178
+ pillBg={
179
+ PROVIDER_META[
180
+ m.provider as keyof typeof PROVIDER_META
181
+ ].pillBg
182
+ }
183
+ size={16}
184
+ />
185
+ ) : (
186
+ <span className="text-[10px] uppercase text-muted-foreground/60">
187
+ {m.provider}
188
+ </span>
189
+ )}
190
+ </button>
191
+ ))}
192
+ </div>
193
+ </>
194
+ )}
195
+ </div>
196
+ )}
197
+
198
+ <div className="h-4 w-px bg-border mx-1" />
199
+
200
+ <button
201
+ type="button"
202
+ onClick={newChat}
203
+ title="New chat"
204
+ className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-secondary hover:text-foreground"
205
+ >
206
+ <SquarePen className="h-[16px] w-[16px]" />
207
+ </button>
208
+ <ChatSettingsModal />
209
+ </div>
210
+ </div>
211
+
212
+ {/* Messages area */}
213
+ <div
214
+ ref={scrollRef}
215
+ className="min-h-0 flex-1 overflow-y-auto [&::-webkit-scrollbar]:hidden"
216
+ >
217
+ <div className="mx-auto w-full max-w-3xl px-4 py-6 sm:px-6">
218
+ {!ready ? (
219
+ <div className="flex flex-col items-center justify-center gap-3 pt-[18vh] text-center">
220
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
221
+ <MessageCircle className="size-7" />
222
+ </div>
223
+ <h1 className="text-xl font-semibold tracking-tight text-foreground">
224
+ Chat not ready yet
225
+ </h1>
226
+ <p className="max-w-md text-sm leading-relaxed text-muted-foreground">
227
+ {status?.blockers?.join(" ") ||
228
+ "Set an API key in Settings to start chatting."}
229
+ </p>
230
+ </div>
231
+ ) : isEmpty ? (
232
+ <div className="flex flex-col items-center justify-center gap-3 pt-[18vh] text-center">
233
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
234
+ <MessageCircle className="size-7" />
235
+ </div>
236
+ <h1 className="text-xl font-semibold tracking-tight text-foreground text-balance">
237
+ Chat with your knowledge base
238
+ </h1>
239
+ <p className="max-w-md text-sm leading-relaxed text-muted-foreground text-pretty">
240
+ Ask questions and get AI-powered answers grounded in your
241
+ indexed documents.
242
+ </p>
243
+ {!status?.indexed && (
244
+ <div className="mt-2 flex items-center gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700">
245
+ <TriangleAlert className="size-3.5 shrink-0" />
246
+ No documents indexed yet, add documents in the Docs page. The
247
+ agent can still answer general questions.
248
+ </div>
249
+ )}
250
+ <div className="mt-4 flex flex-wrap justify-center gap-2">
251
+ {status?.suggestions && status.suggestions.length > 0 ? (
252
+ status.suggestions.map((s) => (
253
+ <button
254
+ key={s}
255
+ type="button"
256
+ onClick={() => {
257
+ sendMessage({ text: s });
258
+ }}
259
+ className="rounded-full border border-border bg-card px-3.5 py-1.5 text-xs text-foreground transition hover:bg-secondary"
260
+ >
261
+ {s}
262
+ </button>
263
+ ))
264
+ ) : (
265
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
266
+ {/* <Loader2 className="size-3 animate-spin" /> Generating
267
+ suggestions... */}
268
+ </div>
269
+ )}
270
+ </div>
271
+ </div>
272
+ ) : (
273
+ <div className="flex flex-col gap-6">
274
+ {messages.map((m, idx) => (
275
+ <MessageItem
276
+ key={m.id}
277
+ message={m}
278
+ isLast={idx === messages.length - 1}
279
+ isStreaming={chatStatus === "streaming"}
280
+ />
281
+ ))}
282
+ {chatStatus === "submitted" ? (
283
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
284
+ <span className="h-2 w-2 animate-pulse rounded-full bg-foreground" />
285
+ Thinking…
286
+ </div>
287
+ ) : null}
288
+ {error ? (
289
+ <div className="flex items-start gap-3 rounded-xl border border-destructive/30 bg-destructive/5 px-4 py-3">
290
+ <TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
291
+ <div className="flex-1">
292
+ <p className="text-sm text-foreground">
293
+ {error.message ||
294
+ "Something went wrong while generating a response."}
295
+ </p>
296
+ <button
297
+ type="button"
298
+ onClick={() => regenerate()}
299
+ className="mt-2 inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-foreground transition hover:bg-secondary"
300
+ >
301
+ <RotateCcw className="h-3.5 w-3.5" />
302
+ Try again
303
+ </button>
304
+ </div>
305
+ </div>
306
+ ) : null}
307
+ </div>
308
+ )}
309
+ </div>
310
+ </div>
311
+
312
+ {/* Input area */}
313
+ {ready && (
314
+ <div className="relative z-10000 px-4 pb-5 pt-3 sm:px-6">
315
+ <form onSubmit={handleSubmit} className="mx-auto w-full max-w-3xl">
316
+ <div className="flex items-end gap-2 rounded-2xl border border-border bg-card p-2 transition focus-within:ring-1 focus-within:ring-ring">
317
+ <textarea
318
+ value={input}
319
+ onChange={(e) => setInput(e.target.value)}
320
+ onKeyDown={(e) => {
321
+ if (e.key === "Enter" && !e.shiftKey) {
322
+ e.preventDefault();
323
+ handleSubmit(e);
324
+ }
325
+ }}
326
+ rows={2}
327
+ disabled={!ready}
328
+ placeholder="Ask about your knowledge base…"
329
+ className="max-h-40 flex-1 resize-none bg-transparent px-2 py-1.5 text-sm text-foreground outline-none placeholder:text-muted-foreground"
330
+ />
331
+ <button
332
+ type="submit"
333
+ disabled={!input.trim() || isBusy}
334
+ className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground transition hover:opacity-90 disabled:opacity-40"
335
+ >
336
+ {isBusy ? (
337
+ <Loader2 className="h-[18px] w-[18px] animate-spin" />
338
+ ) : (
339
+ <ArrowUp className="h-[18px] w-[18px]" />
340
+ )}
341
+ </button>
342
+ </div>
343
+ </form>
344
+ </div>
345
+ )}
346
+ </div>
347
+ );
348
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { ChevronDown, Loader2, Database } from "lucide-react";
5
+
6
+ interface KBHit {
7
+ title: string;
8
+ url: string;
9
+ score: number;
10
+ text: string;
11
+ }
12
+
13
+ /**
14
+ * Renders knowledge base retrieval results from tool-searchKnowledgeBase parts.
15
+ *
16
+ * UIMessage parts for tools look like:
17
+ * { type: "tool-searchKnowledgeBase", state: "input-streaming" | "input-available" | "output", input: { query }, output: { query, hits } }
18
+ */
19
+ export function KnowledgeBaseResult({
20
+ parts,
21
+ isShimmering,
22
+ }: {
23
+ parts: any[];
24
+ isShimmering?: boolean;
25
+ }) {
26
+ const [open, setOpen] = useState(false);
27
+ const isRunning = parts.some(
28
+ (part) =>
29
+ part.state === "input-streaming" || part.state === "input-available",
30
+ );
31
+ const hits: KBHit[] = parts.flatMap((part) => part.output?.hits ?? []);
32
+ const queries = parts.map((part) => part.input?.query).filter(Boolean);
33
+
34
+ return (
35
+ <div className="relative overflow-hidden rounded-xl border border-border bg-card">
36
+ {isShimmering && (
37
+ <div className="pointer-events-none absolute inset-0 z-10 animate-pulse bg-linear-to-r from-transparent via-foreground/5 to-transparent" />
38
+ )}
39
+ <button
40
+ type="button"
41
+ onClick={() => setOpen((o) => !o)}
42
+ className="flex w-full items-center gap-3 px-3.5 py-3 text-left"
43
+ >
44
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-secondary text-foreground">
45
+ {isRunning ? (
46
+ <Loader2 className="h-4 w-4 animate-spin" />
47
+ ) : (
48
+ <Database className="size-4" />
49
+ )}
50
+ </div>
51
+ <div className="min-w-0 flex-1">
52
+ <div className="text-sm font-medium text-foreground">
53
+ {isRunning ? "Searching knowledge base…" : "Knowledge base"}
54
+ </div>
55
+ <div className="truncate text-xs text-muted-foreground">
56
+ {queries.length > 0
57
+ ? `"${queries[0]}"${queries.length > 1 ? ` and ${queries.length - 1} more` : ""}`
58
+ : "RAG retrieval"}
59
+ {!isRunning
60
+ ? ` · ${hits.length} ${hits.length === 1 ? "result" : "results"}`
61
+ : ""}
62
+ </div>
63
+ </div>
64
+ {!isRunning && hits.length > 0 ? (
65
+ <ChevronDown
66
+ className={`h-4 w-4 text-muted-foreground transition ${open ? "rotate-180" : ""}`}
67
+ />
68
+ ) : null}
69
+ </button>
70
+
71
+ {open && hits.length > 0 ? (
72
+ <div className="flex flex-col gap-px border-t border-border bg-border">
73
+ {hits.map((h, i) => (
74
+ <div key={i} className="bg-card px-3.5 py-3">
75
+ <div className="flex items-center justify-between gap-2">
76
+ <span className="truncate text-sm font-medium text-foreground">
77
+ {h.title}
78
+ </span>
79
+ <span className="shrink-0 rounded-full bg-secondary px-2 py-0.5 text-[11px] tabular-nums text-muted-foreground">
80
+ {Math.round(h.score * 100)}%
81
+ </span>
82
+ </div>
83
+ {h.url ? (
84
+ <div className="mt-0.5 truncate text-xs text-muted-foreground">
85
+ {h.url}
86
+ </div>
87
+ ) : null}
88
+ <p className="mt-1.5 line-clamp-3 text-xs leading-relaxed text-muted-foreground">
89
+ {h.text}
90
+ </p>
91
+ </div>
92
+ ))}
93
+ </div>
94
+ ) : null}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import type { UIMessage } from "ai";
4
+ import { KnowledgeBaseResult } from "@/components/chat/knowledge-base-result";
5
+
6
+ export function MessageItem({
7
+ message,
8
+ isLast,
9
+ isStreaming,
10
+ }: {
11
+ message: UIMessage;
12
+ isLast?: boolean;
13
+ isStreaming?: boolean;
14
+ }) {
15
+ const isUser = message.role === "user";
16
+
17
+ if (isUser) {
18
+ const text = message.parts
19
+ .filter((p) => p.type === "text")
20
+ .map((p: any) => p.text)
21
+ .join("");
22
+ return (
23
+ <div className="message user-message flex justify-end" data-role="user">
24
+ <div className="max-w-[85%] rounded-2xl rounded-br-md bg-primary/10 px-4 py-2.5 text-[15px] leading-relaxed text-foreground">
25
+ {text}
26
+ </div>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ // UIMessage parts: "text", "tool-searchKnowledgeBase", etc.
32
+ const kbParts = message.parts.filter(
33
+ (p: any) => p.type === "tool-searchKnowledgeBase",
34
+ );
35
+ const textParts = message.parts.filter((p: any) => p.type === "text");
36
+
37
+ const isShimmering =
38
+ textParts.every((p: any) => !p.text || p.text.trim().length === 0) &&
39
+ isLast &&
40
+ isStreaming;
41
+
42
+ return (
43
+ <div className="message assistant-message flex flex-col gap-3" data-role="assistant">
44
+ {kbParts.length > 0 ? (
45
+ <KnowledgeBaseResult parts={kbParts} isShimmering={isShimmering} />
46
+ ) : null}
47
+
48
+ {textParts.map((part: any, i: number) => (
49
+ <div
50
+ key={i}
51
+ className="prose prose-sm max-w-none text-[15px] text-foreground prose-headings:text-foreground prose-strong:text-foreground prose-a:text-foreground prose-a:underline prose-a:underline-offset-2 prose-code:rounded prose-code:bg-secondary prose-code:px-1 prose-code:py-0.5 prose-code:text-xs prose-code:before:content-none prose-code:after:content-none"
52
+ dangerouslySetInnerHTML={{ __html: renderMarkdown(part.text || "") }}
53
+ />
54
+ ))}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Lightweight markdown → HTML for chat messages.
61
+ */
62
+ function renderMarkdown(text: string): string {
63
+ if (!text) return "";
64
+ return text
65
+ .replace(/&/g, "&amp;")
66
+ .replace(/</g, "&lt;")
67
+ .replace(/>/g, "&gt;")
68
+ // Code blocks
69
+ .replace(
70
+ /```(\w*)\n([\s\S]*?)```/g,
71
+ '<pre class="rounded-lg bg-secondary p-3 text-xs overflow-x-auto my-2"><code>$2</code></pre>',
72
+ )
73
+ // Inline code
74
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
75
+ // Bold
76
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
77
+ // Italic
78
+ .replace(/\*(.+?)\*/g, "<em>$1</em>")
79
+ // Links
80
+ .replace(
81
+ /\[([^\]]+)\]\(([^)]+)\)/g,
82
+ '<a href="$2" target="_blank" rel="noreferrer">$1</a>',
83
+ )
84
+ // Headings
85
+ .replace(
86
+ /^### (.+)$/gm,
87
+ '<h3 class="text-base font-semibold mt-3 mb-1">$1</h3>',
88
+ )
89
+ .replace(
90
+ /^## (.+)$/gm,
91
+ '<h2 class="text-lg font-semibold mt-4 mb-1">$1</h2>',
92
+ )
93
+ .replace(
94
+ /^# (.+)$/gm,
95
+ '<h1 class="text-xl font-semibold mt-4 mb-2">$1</h1>',
96
+ )
97
+ // Lists
98
+ .replace(/^- (.+)$/gm, '<li class="ml-4 list-disc">$1</li>')
99
+ .replace(/^\d+\. (.+)$/gm, '<li class="ml-4 list-decimal">$1</li>')
100
+ // Paragraphs
101
+ .replace(/\n\n/g, "</p><p>")
102
+ .replace(/\n/g, "<br/>");
103
+ }