Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions apps/web/app/(dashboard)/dashboard/playground/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { Send, Square, RotateCcw } from "lucide-react";
import { DocumentPicker } from "@/components/playground/DocumentPicker";
import { ChatThread } from "@/components/playground/ChatThread";
import { RetrievalInspector } from "@/components/playground/RetrievalInspector";
import { CollectionAsk } from "@/components/playground/CollectionAsk";
import { FileText, Library } from "lucide-react";
import { cn } from "@/lib/utils";
import {
useTreewalkStream,
type AssistantMessage,
Expand All @@ -20,6 +23,7 @@ export default function PlaygroundPage() {
const [source, setSource] = useState<"demo" | "mine">("demo");
const [docId, setDocId] = useState("");
const [input, setInput] = useState("");
const [mode, setMode] = useState<"document" | "collection">("document");

const { messages, isStreaming, send, stop, reset } = useTreewalkStream();

Expand Down Expand Up @@ -82,6 +86,29 @@ export default function PlaygroundPage() {
</h1>
</div>
<div className="flex items-center gap-2">
{/* Mode: single document vs whole collection */}
<div className="flex items-center rounded-full border border-border-light bg-muted/40 p-0.5">
{(
[
{ m: "document", label: "Document", Icon: FileText },
{ m: "collection", label: "Collection", Icon: Library },
] as const
).map(({ m, label, Icon }) => (
<button
key={m}
onClick={() => setMode(m)}
className={cn(
"flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[12.5px] font-medium transition-colors",
mode === m
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
<Icon className="h-3.5 w-3.5" />
{label}
</button>
))}
</div>
<DocumentPicker
demo={demo}
mine={mine}
Expand All @@ -90,6 +117,7 @@ export default function PlaygroundPage() {
docId={docId}
onDocChange={setDocId}
loading={loadingDocs}
hideDocSelect={mode === "collection"}
/>
{messages.length > 0 && (
<Button
Expand All @@ -106,7 +134,16 @@ export default function PlaygroundPage() {
</div>
</div>

{/* Collection mode: one synthesized answer across the whole store */}
{mode === "collection" && (
<CollectionAsk
source={source}
docCount={(source === "demo" ? demo : mine).length}
/>
)}

{/* Split: chat (left) + inspector (right) */}
{mode === "document" && (
<div className="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-5">
{/* Chat column */}
<div className="flex min-h-0 flex-col rounded-[20px] border border-border-light bg-card shadow-[rgba(0,0,0,0.04)_0px_2px_8px] lg:col-span-3">
Expand Down Expand Up @@ -164,6 +201,7 @@ export default function PlaygroundPage() {
<RetrievalInspector message={activeAssistant} />
</div>
</div>
)}
</div>
);
}
100 changes: 100 additions & 0 deletions apps/web/app/api/dashboard/playground/store-answer/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from "next/server";
import { forwardToCP } from "@/lib/cp-proxy";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const DEMO_API_KEY = process.env.VECTORLESS_INTERNAL_API_KEY || "";

interface StoreAnswerBody {
source?: "demo" | "mine";
query?: string;
}

interface DocLite {
doc_id: string;
title: string;
status: string;
}

function readyDocs(data: unknown): DocLite[] {
const arr =
(data as { documents?: unknown[] } | null)?.documents ??
(Array.isArray(data) ? (data as unknown[]) : []);
return (arr as Record<string, unknown>[])
.map((d) => ({
doc_id: String(d.doc_id ?? d.document_id ?? d.id ?? ""),
title: String(d.title ?? d.filename ?? "Untitled"),
status: String(d.status ?? "unknown"),
}))
.filter((d) => d.doc_id && d.status === "ready");
}

/**
* POST /api/dashboard/playground/store-answer
* Ask one question across an entire collection (store) and return a
* single synthesised answer with per-document citations.
*
* source "demo" → the shared demo store (internal key); "mine" → the
* caller's selected store (session + store cookie). We resolve the
* store's ready document_ids, hand them to the engine's /v1/answer/store,
* then map each citation's document_id back to its title for display.
*/
export async function POST(request: NextRequest) {
let body: StoreAnswerBody;
try {
body = (await request.json()) as StoreAnswerBody;
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const query = (body.query ?? "").trim();
if (!query) {
return NextResponse.json({ error: "query is required" }, { status: 400 });
}
const useDemo = body.source !== "mine";
const auth = useDemo && DEMO_API_KEY ? { apiKey: DEMO_API_KEY } : {};

// 1. Resolve the collection's ready documents.
const list = await forwardToCP("/v1/documents?limit=100", auth);
if (!list.ok) {
return NextResponse.json(
list.data ?? { error: "Failed to list collection" },
{ status: list.status },
);
}
const docs = readyDocs(list.data);
if (docs.length === 0) {
return NextResponse.json(
{ error: "This collection has no ready documents yet." },
{ status: 400 },
);
}

// 2. Ask across the whole collection.
const ans = await forwardToCP("/v1/answer/store", {
method: "POST",
body: { document_ids: docs.map((d) => d.doc_id), query },
...auth,
});
if (!ans.ok) {
return NextResponse.json(
ans.data ?? { error: "Store answer failed" },
{ status: ans.status },
);
}

// 3. Map citation document_id → title for display.
const titleById = Object.fromEntries(docs.map((d) => [d.doc_id, d.title]));
const data = (ans.data ?? {}) as {
citations?: { document_id?: string; document_title?: string }[];
};
if (Array.isArray(data.citations)) {
data.citations = data.citations.map((c) => ({
...c,
document_title:
c.document_title || titleById[c.document_id ?? ""] || c.document_id,
}));
}

return NextResponse.json({ ...data, collection_size: docs.length });
}
Loading
Loading