business-stack 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/.python-version +1 -0
- package/backend/.env.example +65 -0
- package/backend/alembic/env.py +63 -0
- package/backend/alembic/script.py.mako +26 -0
- package/backend/alembic/versions/2a9c8f1d0e7b_multimodal_kb_schema.py +279 -0
- package/backend/alembic/versions/3c1d2e4f5a6b_sqlite_vec_embeddings.py +58 -0
- package/backend/alembic/versions/4e8b0c2d1a3f_document_links.py +50 -0
- package/backend/alembic/versions/6a0b1c2d3e4f_link_expansion_dedupe_columns.py +49 -0
- package/backend/alembic/versions/7d8e9f0a1b2c_document_chunks.py +70 -0
- package/backend/alembic/versions/8f2a1c0d9e3b_initial_empty_revision.py +22 -0
- package/backend/alembic/versions/9f0a1b2c3d4e_entity_mentions_cooccurrence.py +123 -0
- package/backend/alembic/versions/b1c2d3e4f5a6_pipeline_dedupe_dlq.py +99 -0
- package/backend/alembic/versions/c2d3e4f5061a_chat_sessions_messages.py +59 -0
- package/backend/alembic.ini +42 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/config.py +337 -0
- package/backend/app/connectors/__init__.py +13 -0
- package/backend/app/connectors/base.py +39 -0
- package/backend/app/connectors/builtins.py +51 -0
- package/backend/app/connectors/playwright_session.py +146 -0
- package/backend/app/connectors/registry.py +68 -0
- package/backend/app/connectors/thread_expansion/__init__.py +33 -0
- package/backend/app/connectors/thread_expansion/fakes.py +154 -0
- package/backend/app/connectors/thread_expansion/models.py +113 -0
- package/backend/app/connectors/thread_expansion/reddit.py +53 -0
- package/backend/app/connectors/thread_expansion/twitter.py +49 -0
- package/backend/app/db.py +5 -0
- package/backend/app/dependencies.py +34 -0
- package/backend/app/logging_config.py +35 -0
- package/backend/app/main.py +97 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/gateway_identity.py +17 -0
- package/backend/app/middleware/openapi_gateway.py +71 -0
- package/backend/app/middleware/request_id.py +23 -0
- package/backend/app/openapi_config.py +126 -0
- package/backend/app/routers/__init__.py +0 -0
- package/backend/app/routers/admin_pipeline.py +123 -0
- package/backend/app/routers/chat.py +206 -0
- package/backend/app/routers/chunks.py +36 -0
- package/backend/app/routers/entity_extract.py +31 -0
- package/backend/app/routers/example.py +8 -0
- package/backend/app/routers/gemini_embed.py +58 -0
- package/backend/app/routers/health.py +28 -0
- package/backend/app/routers/ingestion.py +146 -0
- package/backend/app/routers/link_expansion.py +34 -0
- package/backend/app/routers/pipeline_status.py +304 -0
- package/backend/app/routers/query.py +63 -0
- package/backend/app/routers/vectors.py +63 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/canonical.py +44 -0
- package/backend/app/schemas/chat.py +50 -0
- package/backend/app/schemas/ingest.py +29 -0
- package/backend/app/schemas/query.py +153 -0
- package/backend/app/schemas/vectors.py +56 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/chat_store.py +152 -0
- package/backend/app/services/chunking/__init__.py +3 -0
- package/backend/app/services/chunking/llm_boundaries.py +63 -0
- package/backend/app/services/chunking/schemas.py +30 -0
- package/backend/app/services/chunking/semantic_chunk.py +178 -0
- package/backend/app/services/chunking/splitters.py +214 -0
- package/backend/app/services/embeddings/__init__.py +20 -0
- package/backend/app/services/embeddings/build_inputs.py +140 -0
- package/backend/app/services/embeddings/dlq.py +128 -0
- package/backend/app/services/embeddings/gemini_api.py +207 -0
- package/backend/app/services/embeddings/persist.py +74 -0
- package/backend/app/services/embeddings/types.py +32 -0
- package/backend/app/services/embeddings/worker.py +224 -0
- package/backend/app/services/entities/__init__.py +12 -0
- package/backend/app/services/entities/gliner_extract.py +63 -0
- package/backend/app/services/entities/llm_extract.py +94 -0
- package/backend/app/services/entities/pipeline.py +179 -0
- package/backend/app/services/entities/spacy_extract.py +63 -0
- package/backend/app/services/entities/types.py +15 -0
- package/backend/app/services/gemini_chat.py +113 -0
- package/backend/app/services/hooks/__init__.py +3 -0
- package/backend/app/services/hooks/post_ingest.py +186 -0
- package/backend/app/services/ingestion/__init__.py +0 -0
- package/backend/app/services/ingestion/persist.py +188 -0
- package/backend/app/services/integrations_remote.py +91 -0
- package/backend/app/services/link_expansion/__init__.py +3 -0
- package/backend/app/services/link_expansion/canonical_url.py +45 -0
- package/backend/app/services/link_expansion/domain_policy.py +26 -0
- package/backend/app/services/link_expansion/html_extract.py +72 -0
- package/backend/app/services/link_expansion/rate_limit.py +32 -0
- package/backend/app/services/link_expansion/robots.py +46 -0
- package/backend/app/services/link_expansion/schemas.py +67 -0
- package/backend/app/services/link_expansion/worker.py +458 -0
- package/backend/app/services/normalization/__init__.py +7 -0
- package/backend/app/services/normalization/normalizer.py +331 -0
- package/backend/app/services/normalization/persist_normalized.py +67 -0
- package/backend/app/services/playwright_extract/__init__.py +13 -0
- package/backend/app/services/playwright_extract/__main__.py +96 -0
- package/backend/app/services/playwright_extract/extract.py +181 -0
- package/backend/app/services/retrieval_service.py +351 -0
- package/backend/app/sqlite_ext.py +36 -0
- package/backend/app/storage/__init__.py +3 -0
- package/backend/app/storage/blobs.py +30 -0
- package/backend/app/vectorstore/__init__.py +13 -0
- package/backend/app/vectorstore/sqlite_vec_store.py +242 -0
- package/backend/backend.egg-info/PKG-INFO +18 -0
- package/backend/backend.egg-info/SOURCES.txt +93 -0
- package/backend/backend.egg-info/dependency_links.txt +1 -0
- package/backend/backend.egg-info/entry_points.txt +2 -0
- package/backend/backend.egg-info/requires.txt +15 -0
- package/backend/backend.egg-info/top_level.txt +4 -0
- package/backend/package.json +15 -0
- package/backend/pyproject.toml +52 -0
- package/backend/tests/conftest.py +40 -0
- package/backend/tests/test_chat.py +92 -0
- package/backend/tests/test_chunking.py +132 -0
- package/backend/tests/test_entities.py +170 -0
- package/backend/tests/test_gemini_embed.py +224 -0
- package/backend/tests/test_health.py +24 -0
- package/backend/tests/test_ingest_raw.py +123 -0
- package/backend/tests/test_link_expansion.py +241 -0
- package/backend/tests/test_main.py +12 -0
- package/backend/tests/test_normalizer.py +114 -0
- package/backend/tests/test_openapi_gateway.py +40 -0
- package/backend/tests/test_pipeline_hardening.py +285 -0
- package/backend/tests/test_pipeline_status.py +71 -0
- package/backend/tests/test_playwright_extract.py +80 -0
- package/backend/tests/test_post_ingest_hooks.py +162 -0
- package/backend/tests/test_query.py +165 -0
- package/backend/tests/test_thread_expansion.py +72 -0
- package/backend/tests/test_vectors.py +85 -0
- package/backend/uv.lock +1839 -0
- package/bin/business-stack.cjs +412 -0
- package/frontend/web/.env.example +23 -0
- package/frontend/web/AGENTS.md +5 -0
- package/frontend/web/CLAUDE.md +1 -0
- package/frontend/web/README.md +36 -0
- package/frontend/web/components.json +25 -0
- package/frontend/web/next-env.d.ts +6 -0
- package/frontend/web/next.config.ts +30 -0
- package/frontend/web/package.json +65 -0
- package/frontend/web/postcss.config.mjs +7 -0
- package/frontend/web/skills-lock.json +35 -0
- package/frontend/web/src/app/account/[[...path]]/page.tsx +19 -0
- package/frontend/web/src/app/auth/[[...path]]/page.tsx +14 -0
- package/frontend/web/src/app/chat/page.tsx +725 -0
- package/frontend/web/src/app/favicon.ico +0 -0
- package/frontend/web/src/app/globals.css +563 -0
- package/frontend/web/src/app/layout.tsx +50 -0
- package/frontend/web/src/app/page.tsx +96 -0
- package/frontend/web/src/app/settings/integrations/actions.ts +74 -0
- package/frontend/web/src/app/settings/integrations/integrations-settings-form.tsx +330 -0
- package/frontend/web/src/app/settings/integrations/page.tsx +41 -0
- package/frontend/web/src/app/webhooks/alpha-alerts/route.ts +84 -0
- package/frontend/web/src/components/home-auth-panel.tsx +49 -0
- package/frontend/web/src/components/providers.tsx +50 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/registry.ts +35 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/types.ts +8 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/wabridge-delivery.test.ts +40 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/wabridge-delivery.ts +78 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/wabridge.ts +30 -0
- package/frontend/web/src/lib/alpha-webhook/handler.ts +12 -0
- package/frontend/web/src/lib/alpha-webhook/signature.test.ts +33 -0
- package/frontend/web/src/lib/alpha-webhook/signature.ts +21 -0
- package/frontend/web/src/lib/alpha-webhook/types.ts +23 -0
- package/frontend/web/src/lib/auth-client.ts +23 -0
- package/frontend/web/src/lib/integrations-config.ts +125 -0
- package/frontend/web/src/lib/ui-utills.tsx +90 -0
- package/frontend/web/src/lib/utils.ts +6 -0
- package/frontend/web/tsconfig.json +36 -0
- package/frontend/web/tsconfig.tsbuildinfo +1 -0
- package/frontend/web/vitest.config.ts +14 -0
- package/gateway/.env.example +23 -0
- package/gateway/README.md +13 -0
- package/gateway/package.json +24 -0
- package/gateway/src/auth.ts +49 -0
- package/gateway/src/index.ts +141 -0
- package/gateway/src/integrations/admin.ts +19 -0
- package/gateway/src/integrations/crypto.ts +52 -0
- package/gateway/src/integrations/handlers.ts +124 -0
- package/gateway/src/integrations/keys.ts +12 -0
- package/gateway/src/integrations/store.ts +106 -0
- package/gateway/src/stack-secrets.ts +35 -0
- package/gateway/tsconfig.json +13 -0
- package/package.json +33 -0
- package/turbo.json +27 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { authClient } from "@/lib/auth-client";
|
|
6
|
+
|
|
7
|
+
type ChatSessionRow = {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string | null;
|
|
10
|
+
created_at: string;
|
|
11
|
+
updated_at: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ChatMessageRow = {
|
|
15
|
+
id: number;
|
|
16
|
+
role: string;
|
|
17
|
+
content: string;
|
|
18
|
+
meta_json: string | null;
|
|
19
|
+
created_at: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type RawIngestResponse = {
|
|
23
|
+
document_id: string;
|
|
24
|
+
status: string;
|
|
25
|
+
connector: string;
|
|
26
|
+
deduplicated?: boolean;
|
|
27
|
+
normalization_failed?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type PipelineStep = {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
state: "ok" | "warn" | "error" | "pending";
|
|
34
|
+
detail: string | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type EmbeddingDlq = {
|
|
38
|
+
state: string;
|
|
39
|
+
attempt_count: number;
|
|
40
|
+
last_error: string;
|
|
41
|
+
next_retry_at: string | null;
|
|
42
|
+
updated_at: string | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type DocumentPipeline = {
|
|
46
|
+
document_id: string;
|
|
47
|
+
status: string;
|
|
48
|
+
content_type: string | null;
|
|
49
|
+
content_block_count: number;
|
|
50
|
+
chunk_count: number;
|
|
51
|
+
vector_row_count: number;
|
|
52
|
+
gemini_embedding_row_count: number;
|
|
53
|
+
normalization_error: Record<string, unknown> | string | null;
|
|
54
|
+
ingest_meta: Record<string, unknown>;
|
|
55
|
+
dlq: EmbeddingDlq | null;
|
|
56
|
+
steps: PipelineStep[];
|
|
57
|
+
checked_at: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function parseErrorDetail(json: unknown): string {
|
|
61
|
+
if (json !== null && typeof json === "object" && "detail" in json) {
|
|
62
|
+
const d = (json as { detail: unknown }).detail;
|
|
63
|
+
if (typeof d === "string") {
|
|
64
|
+
return d;
|
|
65
|
+
}
|
|
66
|
+
return JSON.stringify(d);
|
|
67
|
+
}
|
|
68
|
+
return "Request failed.";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function backendJson<T>(
|
|
72
|
+
path: string,
|
|
73
|
+
init?: RequestInit,
|
|
74
|
+
): Promise<{ ok: true; data: T } | { ok: false; status: number; message: string }> {
|
|
75
|
+
const headers: HeadersInit = { ...init?.headers };
|
|
76
|
+
if (init?.body != null && !("Content-Type" in (headers as Record<string, string>))) {
|
|
77
|
+
(headers as Record<string, string>)["Content-Type"] = "application/json";
|
|
78
|
+
}
|
|
79
|
+
let res: Response;
|
|
80
|
+
try {
|
|
81
|
+
res = await fetch(`/api/backend${path}`, {
|
|
82
|
+
credentials: "include",
|
|
83
|
+
...init,
|
|
84
|
+
headers,
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
return { ok: false, status: 0, message: "Network error." };
|
|
88
|
+
}
|
|
89
|
+
const rawText = await res.text();
|
|
90
|
+
let parsed: unknown = null;
|
|
91
|
+
if (rawText) {
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(rawText) as unknown;
|
|
94
|
+
} catch {
|
|
95
|
+
parsed = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const message =
|
|
100
|
+
res.status === 401
|
|
101
|
+
? "Sign in to use chat and ingestion."
|
|
102
|
+
: parseErrorDetail(parsed);
|
|
103
|
+
return { ok: false, status: res.status, message };
|
|
104
|
+
}
|
|
105
|
+
return { ok: true, data: parsed as T };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function ChatPage() {
|
|
109
|
+
const { data: session, isPending: sessionPending } = authClient.useSession();
|
|
110
|
+
const [sessions, setSessions] = useState<ChatSessionRow[]>([]);
|
|
111
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
112
|
+
const [messages, setMessages] = useState<ChatMessageRow[]>([]);
|
|
113
|
+
const [listError, setListError] = useState<string | null>(null);
|
|
114
|
+
const [messagesError, setMessagesError] = useState<string | null>(null);
|
|
115
|
+
const [input, setInput] = useState("");
|
|
116
|
+
const [sendBusy, setSendBusy] = useState(false);
|
|
117
|
+
const [ingestText, setIngestText] = useState("");
|
|
118
|
+
const [embedAfterIngest, setEmbedAfterIngest] = useState(true);
|
|
119
|
+
const [ingestBusy, setIngestBusy] = useState(false);
|
|
120
|
+
const [ingestNote, setIngestNote] = useState<string | null>(null);
|
|
121
|
+
const [traceDocId, setTraceDocId] = useState("");
|
|
122
|
+
const [pipeline, setPipeline] = useState<DocumentPipeline | null>(null);
|
|
123
|
+
const [pipelineError, setPipelineError] = useState<string | null>(null);
|
|
124
|
+
const [pipelineBusy, setPipelineBusy] = useState(false);
|
|
125
|
+
const [pipelineAutoRefresh, setPipelineAutoRefresh] = useState(true);
|
|
126
|
+
const [pipelineActionsBusy, setPipelineActionsBusy] = useState(false);
|
|
127
|
+
|
|
128
|
+
const loadSessions = useCallback(async () => {
|
|
129
|
+
setListError(null);
|
|
130
|
+
const r = await backendJson<ChatSessionRow[]>("/chat/sessions");
|
|
131
|
+
if (!r.ok) {
|
|
132
|
+
setListError(r.message);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
setSessions(r.data);
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const loadMessages = useCallback(async (sessionId: string) => {
|
|
139
|
+
setMessagesError(null);
|
|
140
|
+
const r = await backendJson<ChatMessageRow[]>(
|
|
141
|
+
`/chat/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
142
|
+
);
|
|
143
|
+
if (!r.ok) {
|
|
144
|
+
setMessagesError(r.message);
|
|
145
|
+
setMessages([]);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setMessages(r.data);
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!session?.user) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
void loadSessions();
|
|
156
|
+
}, [session?.user, loadSessions]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!activeId || !session?.user) {
|
|
160
|
+
setMessages([]);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
void loadMessages(activeId);
|
|
164
|
+
}, [activeId, session?.user, loadMessages]);
|
|
165
|
+
|
|
166
|
+
const fetchPipeline = useCallback(async (docId: string) => {
|
|
167
|
+
const id = docId.trim();
|
|
168
|
+
if (!id) {
|
|
169
|
+
setPipeline(null);
|
|
170
|
+
setPipelineError(null);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
setPipelineBusy(true);
|
|
174
|
+
setPipelineError(null);
|
|
175
|
+
const r = await backendJson<DocumentPipeline>(
|
|
176
|
+
`/ingest/documents/${encodeURIComponent(id)}/pipeline`,
|
|
177
|
+
);
|
|
178
|
+
setPipelineBusy(false);
|
|
179
|
+
if (!r.ok) {
|
|
180
|
+
setPipeline(null);
|
|
181
|
+
setPipelineError(r.message);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
setPipeline(r.data);
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!session?.user || !traceDocId.trim() || !pipelineAutoRefresh) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
void fetchPipeline(traceDocId);
|
|
192
|
+
const t = window.setInterval(() => {
|
|
193
|
+
void fetchPipeline(traceDocId);
|
|
194
|
+
}, 2500);
|
|
195
|
+
return () => window.clearInterval(t);
|
|
196
|
+
}, [session?.user, traceDocId, pipelineAutoRefresh, fetchPipeline]);
|
|
197
|
+
|
|
198
|
+
const startNewChat = async () => {
|
|
199
|
+
const r = await backendJson<ChatSessionRow>("/chat/sessions", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
body: JSON.stringify({}),
|
|
202
|
+
});
|
|
203
|
+
if (!r.ok) {
|
|
204
|
+
setListError(r.message);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
setSessions((prev) => [r.data, ...prev]);
|
|
208
|
+
setActiveId(r.data.id);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const removeSession = async (id: string) => {
|
|
212
|
+
const r = await backendJson<{ deleted: boolean }>(
|
|
213
|
+
`/chat/sessions/${encodeURIComponent(id)}`,
|
|
214
|
+
{ method: "DELETE" },
|
|
215
|
+
);
|
|
216
|
+
if (!r.ok) {
|
|
217
|
+
setListError(r.message);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
setSessions((prev) => prev.filter((s) => s.id !== id));
|
|
221
|
+
if (activeId === id) {
|
|
222
|
+
setActiveId(null);
|
|
223
|
+
setMessages([]);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const sendMessage = async () => {
|
|
228
|
+
const text = input.trim();
|
|
229
|
+
if (!activeId || !text || sendBusy) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
setSendBusy(true);
|
|
233
|
+
setMessagesError(null);
|
|
234
|
+
const r = await backendJson<{
|
|
235
|
+
reply: string;
|
|
236
|
+
assistant_message_id: number;
|
|
237
|
+
}>(`/chat/sessions/${encodeURIComponent(activeId)}/complete`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: JSON.stringify({ message: text, k: 8 }),
|
|
240
|
+
});
|
|
241
|
+
setSendBusy(false);
|
|
242
|
+
setInput("");
|
|
243
|
+
if (!r.ok) {
|
|
244
|
+
setMessagesError(r.message);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
void loadMessages(activeId);
|
|
248
|
+
void loadSessions();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const runIngest = async () => {
|
|
252
|
+
const body = ingestText.trim();
|
|
253
|
+
if (!body || ingestBusy) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
setIngestBusy(true);
|
|
257
|
+
setIngestNote(null);
|
|
258
|
+
const envelope = {
|
|
259
|
+
source: "web-chat",
|
|
260
|
+
timestamp: new Date().toISOString(),
|
|
261
|
+
content_type: "text" as const,
|
|
262
|
+
payload: body,
|
|
263
|
+
metadata: {},
|
|
264
|
+
};
|
|
265
|
+
const ingest = await backendJson<RawIngestResponse>(
|
|
266
|
+
"/ingest/raw?connector=generic",
|
|
267
|
+
{ method: "POST", body: JSON.stringify(envelope) },
|
|
268
|
+
);
|
|
269
|
+
if (!ingest.ok) {
|
|
270
|
+
setIngestBusy(false);
|
|
271
|
+
setIngestNote(ingest.message);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const docId = ingest.data.document_id;
|
|
275
|
+
const dedup = ingest.data.deduplicated === true;
|
|
276
|
+
const normFail = ingest.data.normalization_failed === true;
|
|
277
|
+
setTraceDocId(docId);
|
|
278
|
+
setPipelineAutoRefresh(true);
|
|
279
|
+
void fetchPipeline(docId);
|
|
280
|
+
if (embedAfterIngest) {
|
|
281
|
+
const emb = await backendJson<{ accepted: boolean }>(
|
|
282
|
+
`/ingest/documents/${encodeURIComponent(docId)}/embed`,
|
|
283
|
+
{ method: "POST", body: JSON.stringify({ multimodal: false }) },
|
|
284
|
+
);
|
|
285
|
+
setIngestBusy(false);
|
|
286
|
+
if (!emb.ok) {
|
|
287
|
+
setIngestNote(
|
|
288
|
+
`Ingested document ${docId}, but embedding failed: ${emb.message}`,
|
|
289
|
+
);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const extra =
|
|
293
|
+
dedup ? " (deduplicated)" : normFail ? " (normalization had issues)" : "";
|
|
294
|
+
setIngestNote(
|
|
295
|
+
`Ingested ${docId}${extra}; embedding queued — watch the pipeline panel below.`,
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
setIngestBusy(false);
|
|
299
|
+
setIngestNote(`Ingested ${docId} (no embed — not in vector index yet).`);
|
|
300
|
+
}
|
|
301
|
+
setIngestText("");
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (sessionPending) {
|
|
305
|
+
return (
|
|
306
|
+
<main className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-4 p-6">
|
|
307
|
+
<p className="text-sm text-neutral-500">Checking session…</p>
|
|
308
|
+
</main>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!session?.user) {
|
|
313
|
+
return (
|
|
314
|
+
<main className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-4 p-6">
|
|
315
|
+
<h1 className="text-2xl font-semibold tracking-tight">Chat</h1>
|
|
316
|
+
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
317
|
+
Sign in to load sessions and talk to your knowledge base.
|
|
318
|
+
</p>
|
|
319
|
+
<Link
|
|
320
|
+
className="inline-flex w-fit items-center justify-center rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-800 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-white"
|
|
321
|
+
href="/auth/sign-in"
|
|
322
|
+
>
|
|
323
|
+
Sign in
|
|
324
|
+
</Link>
|
|
325
|
+
<p className="text-sm">
|
|
326
|
+
<Link
|
|
327
|
+
href="/"
|
|
328
|
+
className="text-neutral-600 underline dark:text-neutral-400"
|
|
329
|
+
>
|
|
330
|
+
Home
|
|
331
|
+
</Link>
|
|
332
|
+
</p>
|
|
333
|
+
</main>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<main className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-4 p-6">
|
|
339
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
340
|
+
<h1 className="text-2xl font-semibold tracking-tight">Chat</h1>
|
|
341
|
+
<Link
|
|
342
|
+
href="/"
|
|
343
|
+
className="text-sm text-neutral-600 underline dark:text-neutral-400"
|
|
344
|
+
>
|
|
345
|
+
Home
|
|
346
|
+
</Link>
|
|
347
|
+
</div>
|
|
348
|
+
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
349
|
+
Messages are stored per user in SQLite. Each reply runs retrieval over
|
|
350
|
+
your embedded documents, then Gemini when configured.
|
|
351
|
+
</p>
|
|
352
|
+
|
|
353
|
+
{listError ? (
|
|
354
|
+
<p className="text-sm text-amber-800 dark:text-amber-200">{listError}</p>
|
|
355
|
+
) : null}
|
|
356
|
+
|
|
357
|
+
<div className="grid min-h-[28rem] gap-4 md:grid-cols-[12rem_1fr]">
|
|
358
|
+
<aside className="flex flex-col gap-2 rounded-lg border border-neutral-200 bg-white/60 p-3 dark:border-neutral-800 dark:bg-neutral-950/40">
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
onClick={() => void startNewChat()}
|
|
362
|
+
className="rounded-md bg-neutral-900 px-3 py-2 text-xs font-medium text-white hover:bg-neutral-800 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-white"
|
|
363
|
+
>
|
|
364
|
+
New chat
|
|
365
|
+
</button>
|
|
366
|
+
<ul className="flex max-h-64 flex-col gap-1 overflow-y-auto text-xs md:max-h-none">
|
|
367
|
+
{sessions.map((s) => (
|
|
368
|
+
<li key={s.id} className="flex items-start gap-1">
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
onClick={() => setActiveId(s.id)}
|
|
372
|
+
className={`min-w-0 flex-1 rounded px-2 py-1 text-left hover:bg-neutral-100 dark:hover:bg-neutral-900 ${
|
|
373
|
+
activeId === s.id
|
|
374
|
+
? "bg-neutral-100 font-medium dark:bg-neutral-900"
|
|
375
|
+
: ""
|
|
376
|
+
}`}
|
|
377
|
+
>
|
|
378
|
+
<span className="line-clamp-2 break-all">
|
|
379
|
+
{s.title?.trim() || "Untitled"}
|
|
380
|
+
</span>
|
|
381
|
+
</button>
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
aria-label="Delete chat"
|
|
385
|
+
onClick={() => void removeSession(s.id)}
|
|
386
|
+
className="shrink-0 rounded px-1 text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/40"
|
|
387
|
+
>
|
|
388
|
+
×
|
|
389
|
+
</button>
|
|
390
|
+
</li>
|
|
391
|
+
))}
|
|
392
|
+
</ul>
|
|
393
|
+
</aside>
|
|
394
|
+
|
|
395
|
+
<div className="flex min-h-0 flex-col gap-3">
|
|
396
|
+
<section className="flex min-h-0 flex-1 flex-col rounded-lg border border-neutral-200 bg-white/60 dark:border-neutral-800 dark:bg-neutral-950/40">
|
|
397
|
+
<div className="border-b border-neutral-200 px-3 py-2 text-xs font-medium text-neutral-500 dark:border-neutral-800">
|
|
398
|
+
{activeId ? `Session ${activeId.slice(0, 8)}…` : "Select or start a chat"}
|
|
399
|
+
</div>
|
|
400
|
+
{messagesError ? (
|
|
401
|
+
<p className="p-3 text-sm text-amber-800 dark:text-amber-200">
|
|
402
|
+
{messagesError}
|
|
403
|
+
</p>
|
|
404
|
+
) : null}
|
|
405
|
+
<div className="flex-1 space-y-3 overflow-y-auto p-3 text-sm">
|
|
406
|
+
{!activeId ? (
|
|
407
|
+
<p className="text-neutral-500">Choose a session on the left.</p>
|
|
408
|
+
) : messages.length === 0 ? (
|
|
409
|
+
<p className="text-neutral-500">No messages yet.</p>
|
|
410
|
+
) : (
|
|
411
|
+
messages.map((m) => (
|
|
412
|
+
<div
|
|
413
|
+
key={m.id}
|
|
414
|
+
className={`rounded-lg px-3 py-2 ${
|
|
415
|
+
m.role === "user"
|
|
416
|
+
? "ml-8 bg-neutral-100 dark:bg-neutral-900"
|
|
417
|
+
: "mr-8 border border-neutral-200 dark:border-neutral-800"
|
|
418
|
+
}`}
|
|
419
|
+
>
|
|
420
|
+
<p className="text-[10px] uppercase tracking-wide text-neutral-400">
|
|
421
|
+
{m.role}
|
|
422
|
+
</p>
|
|
423
|
+
<p className="whitespace-pre-wrap text-neutral-900 dark:text-neutral-100">
|
|
424
|
+
{m.content}
|
|
425
|
+
</p>
|
|
426
|
+
</div>
|
|
427
|
+
))
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
<div className="border-t border-neutral-200 p-3 dark:border-neutral-800">
|
|
431
|
+
<div className="flex gap-2">
|
|
432
|
+
<textarea
|
|
433
|
+
className="min-h-[4rem] flex-1 resize-y rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-950"
|
|
434
|
+
placeholder={
|
|
435
|
+
activeId
|
|
436
|
+
? "Ask something (uses RAG + Gemini)…"
|
|
437
|
+
: "Start a chat first…"
|
|
438
|
+
}
|
|
439
|
+
value={input}
|
|
440
|
+
disabled={!activeId || sendBusy}
|
|
441
|
+
onChange={(e) => setInput(e.target.value)}
|
|
442
|
+
onKeyDown={(e) => {
|
|
443
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
void sendMessage();
|
|
446
|
+
}
|
|
447
|
+
}}
|
|
448
|
+
/>
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
disabled={!activeId || sendBusy || !input.trim()}
|
|
452
|
+
onClick={() => void sendMessage()}
|
|
453
|
+
className="h-fit self-end rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white disabled:opacity-40 dark:bg-neutral-100 dark:text-neutral-900"
|
|
454
|
+
>
|
|
455
|
+
{sendBusy ? "…" : "Send"}
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</section>
|
|
460
|
+
|
|
461
|
+
<section className="rounded-lg border border-neutral-200 bg-white/60 p-3 dark:border-neutral-800 dark:bg-neutral-950/40">
|
|
462
|
+
<h2 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
463
|
+
Ingest text
|
|
464
|
+
</h2>
|
|
465
|
+
<p className="mt-1 text-xs text-neutral-500">
|
|
466
|
+
Adds a document via{" "}
|
|
467
|
+
<code className="rounded bg-neutral-100 px-1 dark:bg-neutral-900">
|
|
468
|
+
POST /ingest/raw
|
|
469
|
+
</code>
|
|
470
|
+
. Optional embed queues chunks for vector search used in chat.
|
|
471
|
+
</p>
|
|
472
|
+
<textarea
|
|
473
|
+
className="mt-2 min-h-[5rem] w-full resize-y rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-950"
|
|
474
|
+
placeholder="Paste notes, snippets, or knowledge to store…"
|
|
475
|
+
value={ingestText}
|
|
476
|
+
disabled={ingestBusy}
|
|
477
|
+
onChange={(e) => setIngestText(e.target.value)}
|
|
478
|
+
/>
|
|
479
|
+
<label className="mt-2 flex cursor-pointer items-center gap-2 text-xs text-neutral-600 dark:text-neutral-400">
|
|
480
|
+
<input
|
|
481
|
+
type="checkbox"
|
|
482
|
+
checked={embedAfterIngest}
|
|
483
|
+
disabled={ingestBusy}
|
|
484
|
+
onChange={(e) => setEmbedAfterIngest(e.target.checked)}
|
|
485
|
+
/>
|
|
486
|
+
Queue embedding after ingest (recommended for RAG)
|
|
487
|
+
</label>
|
|
488
|
+
<button
|
|
489
|
+
type="button"
|
|
490
|
+
disabled={ingestBusy || !ingestText.trim()}
|
|
491
|
+
onClick={() => void runIngest()}
|
|
492
|
+
className="mt-2 rounded-md border border-neutral-300 bg-white px-4 py-2 text-sm font-medium hover:bg-neutral-50 disabled:opacity-40 dark:border-neutral-700 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
|
493
|
+
>
|
|
494
|
+
{ingestBusy ? "Working…" : "Ingest"}
|
|
495
|
+
</button>
|
|
496
|
+
{ingestNote ? (
|
|
497
|
+
<p className="mt-2 text-xs text-neutral-700 dark:text-neutral-300">
|
|
498
|
+
{ingestNote}
|
|
499
|
+
</p>
|
|
500
|
+
) : null}
|
|
501
|
+
</section>
|
|
502
|
+
|
|
503
|
+
<section className="rounded-lg border border-neutral-200 bg-white/60 p-3 dark:border-neutral-800 dark:bg-neutral-950/40">
|
|
504
|
+
<h2 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
505
|
+
Embedding pipeline (debug)
|
|
506
|
+
</h2>
|
|
507
|
+
<p className="mt-1 text-xs text-neutral-500">
|
|
508
|
+
Live view of ingest → blocks → chunks → Gemini + sqlite-vec. Uses{" "}
|
|
509
|
+
<code className="rounded bg-neutral-100 px-1 dark:bg-neutral-900">
|
|
510
|
+
GET /ingest/documents/{id}/pipeline
|
|
511
|
+
</code>
|
|
512
|
+
.
|
|
513
|
+
</p>
|
|
514
|
+
<div className="mt-2 flex flex-wrap items-end gap-2">
|
|
515
|
+
<label className="flex min-w-[12rem] flex-1 flex-col gap-1 text-xs text-neutral-600 dark:text-neutral-400">
|
|
516
|
+
Document ID
|
|
517
|
+
<input
|
|
518
|
+
className="rounded-md border border-neutral-300 bg-white px-2 py-1.5 font-mono text-xs dark:border-neutral-700 dark:bg-neutral-950"
|
|
519
|
+
value={traceDocId}
|
|
520
|
+
onChange={(e) => setTraceDocId(e.target.value)}
|
|
521
|
+
placeholder="Paste UUID after ingest…"
|
|
522
|
+
/>
|
|
523
|
+
</label>
|
|
524
|
+
<button
|
|
525
|
+
type="button"
|
|
526
|
+
disabled={pipelineBusy || !traceDocId.trim()}
|
|
527
|
+
onClick={() => void fetchPipeline(traceDocId)}
|
|
528
|
+
className="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs font-medium hover:bg-neutral-50 disabled:opacity-40 dark:border-neutral-700 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
|
529
|
+
>
|
|
530
|
+
{pipelineBusy ? "Loading…" : "Refresh"}
|
|
531
|
+
</button>
|
|
532
|
+
<label className="flex cursor-pointer items-center gap-2 text-xs text-neutral-600 dark:text-neutral-400">
|
|
533
|
+
<input
|
|
534
|
+
type="checkbox"
|
|
535
|
+
checked={pipelineAutoRefresh}
|
|
536
|
+
onChange={(e) => setPipelineAutoRefresh(e.target.checked)}
|
|
537
|
+
/>
|
|
538
|
+
Auto-refresh (2.5s)
|
|
539
|
+
</label>
|
|
540
|
+
</div>
|
|
541
|
+
{pipelineError ? (
|
|
542
|
+
<p className="mt-2 text-xs text-red-700 dark:text-red-300">
|
|
543
|
+
{pipelineError}
|
|
544
|
+
</p>
|
|
545
|
+
) : null}
|
|
546
|
+
{pipeline ? (
|
|
547
|
+
<div className="mt-3 space-y-3">
|
|
548
|
+
<div className="flex flex-wrap gap-2">
|
|
549
|
+
{pipeline.steps.map((step, i) => {
|
|
550
|
+
const ring =
|
|
551
|
+
step.state === "ok"
|
|
552
|
+
? "border-emerald-500/80 bg-emerald-50 dark:bg-emerald-950/30"
|
|
553
|
+
: step.state === "warn"
|
|
554
|
+
? "border-amber-500/80 bg-amber-50 dark:bg-amber-950/30"
|
|
555
|
+
: step.state === "error"
|
|
556
|
+
? "border-red-500/80 bg-red-50 dark:bg-red-950/30"
|
|
557
|
+
: "border-dashed border-neutral-400 bg-neutral-50 dark:border-neutral-600 dark:bg-neutral-900/40";
|
|
558
|
+
return (
|
|
559
|
+
<div key={step.id} className="flex items-stretch gap-2">
|
|
560
|
+
{i > 0 ? (
|
|
561
|
+
<div
|
|
562
|
+
className="hidden w-4 self-center border-t border-dotted border-neutral-300 sm:block dark:border-neutral-600"
|
|
563
|
+
aria-hidden
|
|
564
|
+
/>
|
|
565
|
+
) : null}
|
|
566
|
+
<div
|
|
567
|
+
className={`max-w-[11rem] rounded-lg border px-2.5 py-2 text-xs ${ring}`}
|
|
568
|
+
>
|
|
569
|
+
<p className="font-medium text-neutral-800 dark:text-neutral-100">
|
|
570
|
+
{step.label}
|
|
571
|
+
</p>
|
|
572
|
+
<p className="mt-0.5 text-[10px] uppercase tracking-wide text-neutral-500">
|
|
573
|
+
{step.state}
|
|
574
|
+
</p>
|
|
575
|
+
{step.detail ? (
|
|
576
|
+
<p className="mt-1 break-words text-[11px] leading-snug text-neutral-600 dark:text-neutral-400">
|
|
577
|
+
{step.detail}
|
|
578
|
+
</p>
|
|
579
|
+
) : null}
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
);
|
|
583
|
+
})}
|
|
584
|
+
</div>
|
|
585
|
+
<dl className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs sm:grid-cols-4">
|
|
586
|
+
<div>
|
|
587
|
+
<dt className="text-neutral-500">Status</dt>
|
|
588
|
+
<dd className="font-mono text-neutral-800 dark:text-neutral-200">
|
|
589
|
+
{pipeline.status}
|
|
590
|
+
</dd>
|
|
591
|
+
</div>
|
|
592
|
+
<div>
|
|
593
|
+
<dt className="text-neutral-500">Blocks</dt>
|
|
594
|
+
<dd className="font-mono text-neutral-800 dark:text-neutral-200">
|
|
595
|
+
{pipeline.content_block_count}
|
|
596
|
+
</dd>
|
|
597
|
+
</div>
|
|
598
|
+
<div>
|
|
599
|
+
<dt className="text-neutral-500">Chunks</dt>
|
|
600
|
+
<dd className="font-mono text-neutral-800 dark:text-neutral-200">
|
|
601
|
+
{pipeline.chunk_count}
|
|
602
|
+
</dd>
|
|
603
|
+
</div>
|
|
604
|
+
<div>
|
|
605
|
+
<dt className="text-neutral-500">Vector rows</dt>
|
|
606
|
+
<dd className="font-mono text-neutral-800 dark:text-neutral-200">
|
|
607
|
+
{pipeline.vector_row_count} / {pipeline.gemini_embedding_row_count}{" "}
|
|
608
|
+
vec / gemini
|
|
609
|
+
</dd>
|
|
610
|
+
</div>
|
|
611
|
+
</dl>
|
|
612
|
+
<p className="text-[10px] text-neutral-400">
|
|
613
|
+
Checked {pipeline.checked_at}
|
|
614
|
+
</p>
|
|
615
|
+
{pipeline.dlq ? (
|
|
616
|
+
<div className="rounded-md border border-red-200 bg-red-50/80 p-2 text-xs text-red-900 dark:border-red-900 dark:bg-red-950/40 dark:text-red-100">
|
|
617
|
+
<p className="font-medium">Embedding DLQ</p>
|
|
618
|
+
<p className="mt-1 font-mono text-[11px]">
|
|
619
|
+
{pipeline.dlq.state} · attempts {pipeline.dlq.attempt_count}
|
|
620
|
+
</p>
|
|
621
|
+
{pipeline.dlq.next_retry_at ? (
|
|
622
|
+
<p className="mt-1 text-[11px]">
|
|
623
|
+
Next retry: {pipeline.dlq.next_retry_at}
|
|
624
|
+
</p>
|
|
625
|
+
) : null}
|
|
626
|
+
<pre className="mt-2 max-h-24 overflow-auto whitespace-pre-wrap break-words text-[11px] opacity-90">
|
|
627
|
+
{pipeline.dlq.last_error}
|
|
628
|
+
</pre>
|
|
629
|
+
</div>
|
|
630
|
+
) : null}
|
|
631
|
+
{(typeof pipeline.ingest_meta.embedding_error === "string"
|
|
632
|
+
? pipeline.ingest_meta.embedding_error
|
|
633
|
+
: null) ? (
|
|
634
|
+
<div className="rounded-md border border-amber-200 bg-amber-50/80 p-2 text-xs text-amber-950 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-100">
|
|
635
|
+
<p className="font-medium">ingest_meta.embedding_error</p>
|
|
636
|
+
<pre className="mt-1 max-h-20 overflow-auto whitespace-pre-wrap break-words text-[11px]">
|
|
637
|
+
{String(pipeline.ingest_meta.embedding_error)}
|
|
638
|
+
</pre>
|
|
639
|
+
</div>
|
|
640
|
+
) : null}
|
|
641
|
+
<div className="flex flex-wrap gap-2">
|
|
642
|
+
<button
|
|
643
|
+
type="button"
|
|
644
|
+
disabled={pipelineActionsBusy || !traceDocId.trim()}
|
|
645
|
+
onClick={async () => {
|
|
646
|
+
setPipelineActionsBusy(true);
|
|
647
|
+
const r = await backendJson<{ chunks_written: number }>(
|
|
648
|
+
`/ingest/documents/${encodeURIComponent(traceDocId.trim())}/chunks`,
|
|
649
|
+
{
|
|
650
|
+
method: "POST",
|
|
651
|
+
body: JSON.stringify({
|
|
652
|
+
use_llm_weak_structure: false,
|
|
653
|
+
}),
|
|
654
|
+
},
|
|
655
|
+
);
|
|
656
|
+
setPipelineActionsBusy(false);
|
|
657
|
+
if (!r.ok) {
|
|
658
|
+
setPipelineError(r.message);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
void fetchPipeline(traceDocId);
|
|
662
|
+
}}
|
|
663
|
+
className="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs font-medium hover:bg-neutral-50 disabled:opacity-40 dark:border-neutral-700 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
|
664
|
+
>
|
|
665
|
+
Rebuild chunks
|
|
666
|
+
</button>
|
|
667
|
+
<button
|
|
668
|
+
type="button"
|
|
669
|
+
disabled={pipelineActionsBusy || !traceDocId.trim()}
|
|
670
|
+
onClick={async () => {
|
|
671
|
+
setPipelineActionsBusy(true);
|
|
672
|
+
const r = await backendJson<{ accepted: boolean }>(
|
|
673
|
+
`/admin/documents/${encodeURIComponent(traceDocId.trim())}/retry-embedding`,
|
|
674
|
+
{
|
|
675
|
+
method: "POST",
|
|
676
|
+
body: JSON.stringify({ multimodal: false }),
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
setPipelineActionsBusy(false);
|
|
680
|
+
if (!r.ok) {
|
|
681
|
+
setPipelineError(r.message);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
void fetchPipeline(traceDocId);
|
|
685
|
+
}}
|
|
686
|
+
className="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs font-medium hover:bg-neutral-50 disabled:opacity-40 dark:border-neutral-700 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
|
687
|
+
>
|
|
688
|
+
Retry embedding (admin)
|
|
689
|
+
</button>
|
|
690
|
+
<button
|
|
691
|
+
type="button"
|
|
692
|
+
disabled={pipelineActionsBusy || !traceDocId.trim()}
|
|
693
|
+
onClick={async () => {
|
|
694
|
+
setPipelineActionsBusy(true);
|
|
695
|
+
const r = await backendJson<{ accepted: boolean }>(
|
|
696
|
+
`/ingest/documents/${encodeURIComponent(traceDocId.trim())}/embed`,
|
|
697
|
+
{
|
|
698
|
+
method: "POST",
|
|
699
|
+
body: JSON.stringify({ multimodal: false }),
|
|
700
|
+
},
|
|
701
|
+
);
|
|
702
|
+
setPipelineActionsBusy(false);
|
|
703
|
+
if (!r.ok) {
|
|
704
|
+
setPipelineError(r.message);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
void fetchPipeline(traceDocId);
|
|
708
|
+
}}
|
|
709
|
+
className="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs font-medium hover:bg-neutral-50 disabled:opacity-40 dark:border-neutral-700 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
|
710
|
+
>
|
|
711
|
+
Queue embed again
|
|
712
|
+
</button>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
) : (
|
|
716
|
+
<p className="mt-2 text-xs text-neutral-500">
|
|
717
|
+
Enter a document id and refresh to load pipeline state.
|
|
718
|
+
</p>
|
|
719
|
+
)}
|
|
720
|
+
</section>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
</main>
|
|
724
|
+
);
|
|
725
|
+
}
|