feat(UI): Adding linter and prettier for UI (#3156)

This commit is contained in:
Francisco Arceo 2025-08-14 15:58:43 -06:00 committed by GitHub
parent 61582f327c
commit e69acbafbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1452 additions and 1226 deletions

View file

@ -47,7 +47,7 @@ async function proxyRequest(request: NextRequest, method: string) {
const responseText = await response.text();
console.log(
`Response from FastAPI: ${response.status} ${response.statusText}`,
`Response from FastAPI: ${response.status} ${response.statusText}`
);
// Create response with same status and headers
@ -74,7 +74,7 @@ async function proxyRequest(request: NextRequest, method: string) {
backend_url: BACKEND_URL,
timestamp: new Date().toISOString(),
},
{ status: 500 },
{ status: 500 }
);
}
}

View file

@ -51,9 +51,9 @@ export default function SignInPage() {
onClick={() => {
console.log("Signing in with GitHub...");
signIn("github", { callbackUrl: "/auth/signin" }).catch(
(error) => {
error => {
console.error("Sign in error:", error);
},
}
);
}}
className="w-full"

View file

@ -29,14 +29,13 @@ export default function ChatPlaygroundPage() {
const isModelsLoading = modelsLoading ?? true;
useEffect(() => {
const fetchModels = async () => {
try {
setModelsLoading(true);
setModelsError(null);
const modelList = await client.models.list();
const llmModels = modelList.filter(model => model.model_type === 'llm');
const llmModels = modelList.filter(model => model.model_type === "llm");
setModels(llmModels);
if (llmModels.length > 0) {
setSelectedModel(llmModels[0].identifier);
@ -53,103 +52,122 @@ export default function ChatPlaygroundPage() {
}, [client]);
const extractTextContent = (content: unknown): string => {
if (typeof content === 'string') {
if (typeof content === "string") {
return content;
}
if (Array.isArray(content)) {
return content
.filter(item => item && typeof item === 'object' && 'type' in item && item.type === 'text')
.map(item => (item && typeof item === 'object' && 'text' in item) ? String(item.text) : '')
.join('');
.filter(
item =>
item &&
typeof item === "object" &&
"type" in item &&
item.type === "text"
)
.map(item =>
item && typeof item === "object" && "text" in item
? String(item.text)
: ""
)
.join("");
}
if (content && typeof content === 'object' && 'type' in content && content.type === 'text' && 'text' in content) {
return String(content.text) || '';
if (
content &&
typeof content === "object" &&
"type" in content &&
content.type === "text" &&
"text" in content
) {
return String(content.text) || "";
}
return '';
return "";
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value);
};
const handleSubmit = async (event?: { preventDefault?: () => void }) => {
event?.preventDefault?.();
if (!input.trim()) return;
const handleSubmit = async (event?: { preventDefault?: () => void }) => {
event?.preventDefault?.();
if (!input.trim()) return;
// Add user message to chat
const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: input.trim(),
createdAt: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInput("");
// Use the helper function with the content
await handleSubmitWithContent(userMessage.content);
};
const handleSubmitWithContent = async (content: string) => {
setIsGenerating(true);
setError(null);
try {
const messageParams: CompletionCreateParams["messages"] = [
...messages.map(msg => {
const msgContent = typeof msg.content === 'string' ? msg.content : extractTextContent(msg.content);
if (msg.role === "user") {
return { role: "user" as const, content: msgContent };
} else if (msg.role === "assistant") {
return { role: "assistant" as const, content: msgContent };
} else {
return { role: "system" as const, content: msgContent };
}
}),
{ role: "user" as const, content }
];
const response = await client.chat.completions.create({
model: selectedModel,
messages: messageParams,
stream: true,
});
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: "",
// Add user message to chat
const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: input.trim(),
createdAt: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
let fullContent = "";
for await (const chunk of response) {
if (chunk.choices && chunk.choices[0]?.delta?.content) {
const deltaContent = chunk.choices[0].delta.content;
fullContent += deltaContent;
setMessages(prev => [...prev, userMessage]);
setInput("");
flushSync(() => {
setMessages(prev => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage.role === "assistant") {
lastMessage.content = fullContent;
}
return newMessages;
// Use the helper function with the content
await handleSubmitWithContent(userMessage.content);
};
const handleSubmitWithContent = async (content: string) => {
setIsGenerating(true);
setError(null);
try {
const messageParams: CompletionCreateParams["messages"] = [
...messages.map(msg => {
const msgContent =
typeof msg.content === "string"
? msg.content
: extractTextContent(msg.content);
if (msg.role === "user") {
return { role: "user" as const, content: msgContent };
} else if (msg.role === "assistant") {
return { role: "assistant" as const, content: msgContent };
} else {
return { role: "system" as const, content: msgContent };
}
}),
{ role: "user" as const, content },
];
const response = await client.chat.completions.create({
model: selectedModel,
messages: messageParams,
stream: true,
});
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: "",
createdAt: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
let fullContent = "";
for await (const chunk of response) {
if (chunk.choices && chunk.choices[0]?.delta?.content) {
const deltaContent = chunk.choices[0].delta.content;
fullContent += deltaContent;
flushSync(() => {
setMessages(prev => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage.role === "assistant") {
lastMessage.content = fullContent;
}
return newMessages;
});
});
});
}
}
} catch (err) {
console.error("Error sending message:", err);
setError("Failed to send message. Please try again.");
setMessages(prev => prev.slice(0, -1));
} finally {
setIsGenerating(false);
}
} catch (err) {
console.error("Error sending message:", err);
setError("Failed to send message. Please try again.");
setMessages(prev => prev.slice(0, -1));
} finally {
setIsGenerating(false);
}
};
};
const suggestions = [
"Write a Python function that prints 'Hello, World!'",
"Explain step-by-step how to solve this math problem: If x² + 6x + 9 = 25, what is x?",
@ -163,7 +181,7 @@ const handleSubmitWithContent = async (content: string) => {
content: message.content,
createdAt: new Date(),
};
setMessages(prev => [...prev, newMessage])
setMessages(prev => [...prev, newMessage]);
handleSubmitWithContent(newMessage.content);
};
@ -177,12 +195,20 @@ const handleSubmitWithContent = async (content: string) => {
<div className="mb-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">Chat Playground (Completions)</h1>
<div className="flex gap-2">
<Select value={selectedModel} onValueChange={setSelectedModel} disabled={isModelsLoading || isGenerating}>
<Select
value={selectedModel}
onValueChange={setSelectedModel}
disabled={isModelsLoading || isGenerating}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={isModelsLoading ? "Loading models..." : "Select Model"} />
<SelectValue
placeholder={
isModelsLoading ? "Loading models..." : "Select Model"
}
/>
</SelectTrigger>
<SelectContent>
{models.map((model) => (
{models.map(model => (
<SelectItem key={model.identifier} value={model.identifier}>
{model.identifier}
</SelectItem>

View file

@ -33,12 +33,12 @@ export default function ChatCompletionDetailPage() {
} catch (err) {
console.error(
`Error fetching chat completion detail for ID ${id}:`,
err,
err
);
setError(
err instanceof Error
? err
: new Error("Failed to fetch completion detail"),
: new Error("Failed to fetch completion detail")
);
} finally {
setIsLoading(false);

View file

@ -13,10 +13,10 @@ export default function ResponseDetailPage() {
const client = useAuthClient();
const [responseDetail, setResponseDetail] = useState<OpenAIResponse | null>(
null,
null
);
const [inputItems, setInputItems] = useState<InputItemListResponse | null>(
null,
null
);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isLoadingInputItems, setIsLoadingInputItems] = useState<boolean>(true);
@ -25,7 +25,7 @@ export default function ResponseDetailPage() {
// Helper function to convert ResponseObject to OpenAIResponse
const convertResponseObject = (
responseData: ResponseObject,
responseData: ResponseObject
): OpenAIResponse => {
return {
id: responseData.id,
@ -73,12 +73,12 @@ export default function ResponseDetailPage() {
} else {
console.error(
`Error fetching response detail for ID ${id}:`,
responseResult.reason,
responseResult.reason
);
setError(
responseResult.reason instanceof Error
? responseResult.reason
: new Error("Failed to fetch response detail"),
: new Error("Failed to fetch response detail")
);
}
@ -90,18 +90,18 @@ export default function ResponseDetailPage() {
} else {
console.error(
`Error fetching input items for response ID ${id}:`,
inputItemsResult.reason,
inputItemsResult.reason
);
setInputItemsError(
inputItemsResult.reason instanceof Error
? inputItemsResult.reason
: new Error("Failed to fetch input items"),
: new Error("Failed to fetch input items")
);
}
} catch (err) {
console.error(`Unexpected error fetching data for ID ${id}:`, err);
setError(
err instanceof Error ? err : new Error("Unexpected error occurred"),
err instanceof Error ? err : new Error("Unexpected error occurred")
);
} finally {
setIsLoading(false);

View file

@ -18,7 +18,10 @@ import {
PropertiesCard,
PropertyItem,
} from "@/components/layout/detail-layout";
import { PageBreadcrumb, BreadcrumbSegment } from "@/components/layout/page-breadcrumb";
import {
PageBreadcrumb,
BreadcrumbSegment,
} from "@/components/layout/page-breadcrumb";
export default function ContentDetailPage() {
const params = useParams();
@ -28,13 +31,13 @@ export default function ContentDetailPage() {
const contentId = params.contentId as string;
const client = useAuthClient();
const getTextFromContent = (content: any): string => {
if (typeof content === 'string') {
const getTextFromContent = (content: unknown): string => {
if (typeof content === "string") {
return content;
} else if (content && content.type === 'text') {
} else if (content && content.type === "text") {
return content.text;
}
return '';
return "";
};
const [store, setStore] = useState<VectorStore | null>(null);
@ -44,7 +47,9 @@ export default function ContentDetailPage() {
const [error, setError] = useState<Error | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [editedContent, setEditedContent] = useState("");
const [editedMetadata, setEditedMetadata] = useState<Record<string, any>>({});
const [editedMetadata, setEditedMetadata] = useState<Record<string, unknown>>(
{}
);
const [isEditingEmbedding, setIsEditingEmbedding] = useState(false);
const [editedEmbedding, setEditedEmbedding] = useState<number[]>([]);
@ -64,8 +69,13 @@ export default function ContentDetailPage() {
setFile(fileResponse as VectorStoreFile);
const contentsAPI = new ContentsAPI(client);
const contentsResponse = await contentsAPI.listContents(vectorStoreId, fileId);
const targetContent = contentsResponse.data.find(c => c.id === contentId);
const contentsResponse = await contentsAPI.listContents(
vectorStoreId,
fileId
);
const targetContent = contentsResponse.data.find(
c => c.id === contentId
);
if (targetContent) {
setContent(targetContent);
@ -76,7 +86,9 @@ export default function ContentDetailPage() {
throw new Error(`Content ${contentId} not found`);
}
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to load content."));
setError(
err instanceof Error ? err : new Error("Failed to load content.")
);
} finally {
setIsLoading(false);
}
@ -88,7 +100,8 @@ export default function ContentDetailPage() {
if (!content) return;
try {
const updates: { content?: string; metadata?: Record<string, any> } = {};
const updates: { content?: string; metadata?: Record<string, unknown> } =
{};
if (editedContent !== getTextFromContent(content.content)) {
updates.content = editedContent;
@ -100,25 +113,32 @@ export default function ContentDetailPage() {
if (Object.keys(updates).length > 0) {
const contentsAPI = new ContentsAPI(client);
const updatedContent = await contentsAPI.updateContent(vectorStoreId, fileId, contentId, updates);
const updatedContent = await contentsAPI.updateContent(
vectorStoreId,
fileId,
contentId,
updates
);
setContent(updatedContent);
}
setIsEditing(false);
} catch (err) {
console.error('Failed to update content:', err);
console.error("Failed to update content:", err);
}
};
const handleDelete = async () => {
if (!confirm('Are you sure you want to delete this content?')) return;
if (!confirm("Are you sure you want to delete this content?")) return;
try {
const contentsAPI = new ContentsAPI(client);
await contentsAPI.deleteContent(vectorStoreId, fileId, contentId);
router.push(`/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents`);
router.push(
`/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents`
);
} catch (err) {
console.error('Failed to delete content:', err);
console.error("Failed to delete content:", err);
}
};
@ -134,10 +154,19 @@ export default function ContentDetailPage() {
const breadcrumbSegments: BreadcrumbSegment[] = [
{ label: "Vector Stores", href: "/logs/vector-stores" },
{ label: store?.name || vectorStoreId, href: `/logs/vector-stores/${vectorStoreId}` },
{
label: store?.name || vectorStoreId,
href: `/logs/vector-stores/${vectorStoreId}`,
},
{ label: "Files", href: `/logs/vector-stores/${vectorStoreId}` },
{ label: fileId, href: `/logs/vector-stores/${vectorStoreId}/files/${fileId}` },
{ label: "Contents", href: `/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents` },
{
label: fileId,
href: `/logs/vector-stores/${vectorStoreId}/files/${fileId}`,
},
{
label: "Contents",
href: `/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents`,
},
{ label: contentId },
];
@ -186,7 +215,7 @@ export default function ContentDetailPage() {
{isEditing ? (
<textarea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
onChange={e => setEditedContent(e.target.value)}
className="w-full h-64 p-3 border rounded-md resize-none font-mono text-sm"
placeholder="Enter content..."
/>
@ -206,16 +235,23 @@ export default function ContentDetailPage() {
<div className="flex gap-2">
{isEditingEmbedding ? (
<>
<Button size="sm" onClick={() => {
setIsEditingEmbedding(false);
}}>
<Button
size="sm"
onClick={() => {
setIsEditingEmbedding(false);
}}
>
<Save className="h-4 w-4 mr-1" />
Save
</Button>
<Button size="sm" variant="outline" onClick={() => {
setEditedEmbedding(content?.embedding || []);
setIsEditingEmbedding(false);
}}>
<Button
size="sm"
variant="outline"
onClick={() => {
setEditedEmbedding(content?.embedding || []);
setIsEditingEmbedding(false);
}}
>
<X className="h-4 w-4 mr-1" />
Cancel
</Button>
@ -237,14 +273,16 @@ export default function ContentDetailPage() {
</p>
<textarea
value={JSON.stringify(editedEmbedding, null, 2)}
onChange={(e) => {
onChange={e => {
try {
const parsed = JSON.parse(e.target.value);
if (Array.isArray(parsed) && parsed.every(v => typeof v === 'number')) {
if (
Array.isArray(parsed) &&
parsed.every(v => typeof v === "number")
) {
setEditedEmbedding(parsed);
}
} catch {
}
} catch {}
}}
className="w-full h-32 p-3 border rounded-md resize-none font-mono text-xs"
placeholder="Enter embedding as JSON array..."
@ -259,8 +297,15 @@ export default function ContentDetailPage() {
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md max-h-32 overflow-y-auto">
<pre className="whitespace-pre-wrap font-mono text-xs text-gray-900 dark:text-gray-100">
[{content.embedding.slice(0, 20).map(v => v.toFixed(6)).join(', ')}
{content.embedding.length > 20 ? `\n... and ${content.embedding.length - 20} more values` : ''}]
[
{content.embedding
.slice(0, 20)
.map(v => v.toFixed(6))
.join(", ")}
{content.embedding.length > 20
? `\n... and ${content.embedding.length - 20} more values`
: ""}
]
</pre>
</div>
</div>
@ -284,7 +329,7 @@ export default function ContentDetailPage() {
<div key={key} className="flex gap-2">
<Input
value={key}
onChange={(e) => {
onChange={e => {
const newMetadata = { ...editedMetadata };
delete newMetadata[key];
newMetadata[e.target.value] = value;
@ -294,11 +339,13 @@ export default function ContentDetailPage() {
className="flex-1"
/>
<Input
value={typeof value === 'string' ? value : JSON.stringify(value)}
onChange={(e) => {
value={
typeof value === "string" ? value : JSON.stringify(value)
}
onChange={e => {
setEditedMetadata({
...editedMetadata,
[key]: e.target.value
[key]: e.target.value,
});
}}
placeholder="Value"
@ -312,7 +359,7 @@ export default function ContentDetailPage() {
onClick={() => {
setEditedMetadata({
...editedMetadata,
['']: ''
[""]: "",
});
}}
>
@ -325,7 +372,7 @@ export default function ContentDetailPage() {
<div key={key} className="flex justify-between py-1">
<span className="font-medium text-gray-600">{key}:</span>
<span className="font-mono text-sm">
{typeof value === 'string' ? value : JSON.stringify(value)}
{typeof value === "string" ? value : JSON.stringify(value)}
</span>
</div>
))}
@ -351,15 +398,15 @@ export default function ContentDetailPage() {
value={`${getTextFromContent(content.content).length} chars`}
/>
{content.metadata.chunk_window && (
<PropertyItem
label="Position"
value={content.metadata.chunk_window}
/>
<PropertyItem label="Position" value={content.metadata.chunk_window} />
)}
{file && (
<>
<PropertyItem label="File Status" value={file.status} />
<PropertyItem label="File Usage" value={`${file.usage_bytes} bytes`} />
<PropertyItem
label="File Usage"
value={`${file.usage_bytes} bytes`}
/>
</>
)}
{store && (

View file

@ -18,7 +18,10 @@ import {
PropertiesCard,
PropertyItem,
} from "@/components/layout/detail-layout";
import { PageBreadcrumb, BreadcrumbSegment } from "@/components/layout/page-breadcrumb";
import {
PageBreadcrumb,
BreadcrumbSegment,
} from "@/components/layout/page-breadcrumb";
import {
Table,
TableBody,
@ -36,23 +39,21 @@ export default function ContentsListPage() {
const fileId = params.fileId as string;
const client = useAuthClient();
const getTextFromContent = (content: any): string => {
if (typeof content === 'string') {
const getTextFromContent = (content: unknown): string => {
if (typeof content === "string") {
return content;
} else if (content && content.type === 'text') {
} else if (content && content.type === "text") {
return content.text;
}
return '';
return "";
};
const [store, setStore] = useState<VectorStore | null>(null);
const [file, setFile] = useState<VectorStoreFile | null>(null);
const [contents, setContents] = useState<VectorStoreContentItem[]>([]);
const [isLoadingStore, setIsLoadingStore] = useState(true);
const [isLoadingFile, setIsLoadingFile] = useState(true);
const [isLoadingContents, setIsLoadingContents] = useState(true);
const [errorStore, setErrorStore] = useState<Error | null>(null);
const [errorFile, setErrorFile] = useState<Error | null>(null);
const [errorContents, setErrorContents] = useState<Error | null>(null);
useEffect(() => {
@ -65,7 +66,9 @@ export default function ContentsListPage() {
const response = await client.vectorStores.retrieve(vectorStoreId);
setStore(response as VectorStore);
} catch (err) {
setErrorStore(err instanceof Error ? err : new Error("Failed to load vector store."));
setErrorStore(
err instanceof Error ? err : new Error("Failed to load vector store.")
);
} finally {
setIsLoadingStore(false);
}
@ -80,10 +83,15 @@ export default function ContentsListPage() {
setIsLoadingFile(true);
setErrorFile(null);
try {
const response = await client.vectorStores.files.retrieve(vectorStoreId, fileId);
const response = await client.vectorStores.files.retrieve(
vectorStoreId,
fileId
);
setFile(response as VectorStoreFile);
} catch (err) {
setErrorFile(err instanceof Error ? err : new Error("Failed to load file."));
setErrorFile(
err instanceof Error ? err : new Error("Failed to load file.")
);
} finally {
setIsLoadingFile(false);
}
@ -99,10 +107,16 @@ export default function ContentsListPage() {
setErrorContents(null);
try {
const contentsAPI = new ContentsAPI(client);
const contentsResponse = await contentsAPI.listContents(vectorStoreId, fileId, { limit: 100 });
const contentsResponse = await contentsAPI.listContents(
vectorStoreId,
fileId,
{ limit: 100 }
);
setContents(contentsResponse.data);
} catch (err) {
setErrorContents(err instanceof Error ? err : new Error("Failed to load contents."));
setErrorContents(
err instanceof Error ? err : new Error("Failed to load contents.")
);
} finally {
setIsLoadingContents(false);
}
@ -116,26 +130,36 @@ export default function ContentsListPage() {
await contentsAPI.deleteContent(vectorStoreId, fileId, contentId);
setContents(contents.filter(content => content.id !== contentId));
} catch (err) {
console.error('Failed to delete content:', err);
console.error("Failed to delete content:", err);
}
};
const handleViewContent = (contentId: string) => {
router.push(`/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents/${contentId}`);
router.push(
`/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents/${contentId}`
);
};
const title = `Contents in File: ${fileId}`;
const breadcrumbSegments: BreadcrumbSegment[] = [
{ label: "Vector Stores", href: "/logs/vector-stores" },
{ label: store?.name || vectorStoreId, href: `/logs/vector-stores/${vectorStoreId}` },
{
label: store?.name || vectorStoreId,
href: `/logs/vector-stores/${vectorStoreId}`,
},
{ label: "Files", href: `/logs/vector-stores/${vectorStoreId}` },
{ label: fileId, href: `/logs/vector-stores/${vectorStoreId}/files/${fileId}` },
{
label: fileId,
href: `/logs/vector-stores/${vectorStoreId}/files/${fileId}`,
},
{ label: "Contents" },
];
if (errorStore) {
return <DetailErrorView title={title} id={vectorStoreId} error={errorStore} />;
return (
<DetailErrorView title={title} id={vectorStoreId} error={errorStore} />
);
}
if (isLoadingStore) {
return <DetailLoadingView title={title} />;
@ -175,7 +199,7 @@ export default function ContentsListPage() {
</TableRow>
</TableHeader>
<TableBody>
{contents.map((content) => (
{contents.map(content => (
<TableRow key={content.id}>
<TableCell className="font-mono text-xs">
<Button
@ -189,7 +213,10 @@ export default function ContentsListPage() {
</TableCell>
<TableCell>
<div className="max-w-md">
<p className="text-sm truncate" title={getTextFromContent(content.content)}>
<p
className="text-sm truncate"
title={getTextFromContent(content.content)}
>
{getTextFromContent(content.content)}
</p>
</div>
@ -197,12 +224,25 @@ export default function ContentsListPage() {
<TableCell className="text-xs text-gray-500">
{content.embedding && content.embedding.length > 0 ? (
<div className="max-w-xs">
<span className="font-mono text-xs bg-gray-100 dark:bg-gray-800 rounded px-1 py-0.5" title={`${content.embedding.length}D vector: [${content.embedding.slice(0, 3).map(v => v.toFixed(3)).join(', ')}...]`}>
[{content.embedding.slice(0, 3).map(v => v.toFixed(3)).join(', ')}...] ({content.embedding.length}D)
<span
className="font-mono text-xs bg-gray-100 dark:bg-gray-800 rounded px-1 py-0.5"
title={`${content.embedding.length}D vector: [${content.embedding
.slice(0, 3)
.map(v => v.toFixed(3))
.join(", ")}...]`}
>
[
{content.embedding
.slice(0, 3)
.map(v => v.toFixed(3))
.join(", ")}
...] ({content.embedding.length}D)
</span>
</div>
) : (
<span className="text-gray-400 dark:text-gray-500 italic">No embedding</span>
<span className="text-gray-400 dark:text-gray-500 italic">
No embedding
</span>
)}
</TableCell>
<TableCell className="text-xs text-gray-500">
@ -211,7 +251,9 @@ export default function ContentsListPage() {
: `${content.metadata.content_length || 0} chars`}
</TableCell>
<TableCell className="text-xs">
{new Date(content.created_timestamp * 1000).toLocaleString()}
{new Date(
content.created_timestamp * 1000
).toLocaleString()}
</TableCell>
<TableCell>
<div className="flex gap-1">

View file

@ -4,9 +4,12 @@ import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useAuthClient } from "@/hooks/use-auth-client";
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
import type { VectorStoreFile, FileContentResponse } from "llama-stack-client/resources/vector-stores/files";
import type {
VectorStoreFile,
FileContentResponse,
} from "llama-stack-client/resources/vector-stores/files";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from '@/components/ui/skeleton';
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { List } from "lucide-react";
import {
@ -17,7 +20,10 @@ import {
PropertiesCard,
PropertyItem,
} from "@/components/layout/detail-layout";
import { PageBreadcrumb, BreadcrumbSegment } from "@/components/layout/page-breadcrumb";
import {
PageBreadcrumb,
BreadcrumbSegment,
} from "@/components/layout/page-breadcrumb";
export default function FileDetailPage() {
const params = useParams();
@ -46,7 +52,9 @@ export default function FileDetailPage() {
const response = await client.vectorStores.retrieve(vectorStoreId);
setStore(response as VectorStore);
} catch (err) {
setErrorStore(err instanceof Error ? err : new Error("Failed to load vector store."));
setErrorStore(
err instanceof Error ? err : new Error("Failed to load vector store.")
);
} finally {
setIsLoadingStore(false);
}
@ -61,10 +69,15 @@ export default function FileDetailPage() {
setIsLoadingFile(true);
setErrorFile(null);
try {
const response = await client.vectorStores.files.retrieve(vectorStoreId, fileId);
const response = await client.vectorStores.files.retrieve(
vectorStoreId,
fileId
);
setFile(response as VectorStoreFile);
} catch (err) {
setErrorFile(err instanceof Error ? err : new Error("Failed to load file."));
setErrorFile(
err instanceof Error ? err : new Error("Failed to load file.")
);
} finally {
setIsLoadingFile(false);
}
@ -79,10 +92,15 @@ export default function FileDetailPage() {
setIsLoadingContents(true);
setErrorContents(null);
try {
const response = await client.vectorStores.files.content(vectorStoreId, fileId);
const response = await client.vectorStores.files.content(
vectorStoreId,
fileId
);
setContents(response);
} catch (err) {
setErrorContents(err instanceof Error ? err : new Error("Failed to load contents."));
setErrorContents(
err instanceof Error ? err : new Error("Failed to load contents.")
);
} finally {
setIsLoadingContents(false);
}
@ -91,20 +109,27 @@ export default function FileDetailPage() {
}, [vectorStoreId, fileId, client]);
const handleViewContents = () => {
router.push(`/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents`);
router.push(
`/logs/vector-stores/${vectorStoreId}/files/${fileId}/contents`
);
};
const title = `File: ${fileId}`;
const breadcrumbSegments: BreadcrumbSegment[] = [
{ label: "Vector Stores", href: "/logs/vector-stores" },
{ label: store?.name || vectorStoreId, href: `/logs/vector-stores/${vectorStoreId}` },
{
label: store?.name || vectorStoreId,
href: `/logs/vector-stores/${vectorStoreId}`,
},
{ label: "Files", href: `/logs/vector-stores/${vectorStoreId}` },
{ label: fileId },
];
if (errorStore) {
return <DetailErrorView title={title} id={vectorStoreId} error={errorStore} />;
return (
<DetailErrorView title={title} id={vectorStoreId} error={errorStore} />
);
}
if (isLoadingStore) {
return <DetailLoadingView title={title} />;
@ -136,19 +161,29 @@ export default function FileDetailPage() {
<h3 className="text-lg font-medium mb-2">File Details</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-600 dark:text-gray-400">Status:</span>
<span className="font-medium text-gray-600 dark:text-gray-400">
Status:
</span>
<span className="ml-2">{file.status}</span>
</div>
<div>
<span className="font-medium text-gray-600 dark:text-gray-400">Size:</span>
<span className="font-medium text-gray-600 dark:text-gray-400">
Size:
</span>
<span className="ml-2">{file.usage_bytes} bytes</span>
</div>
<div>
<span className="font-medium text-gray-600 dark:text-gray-400">Created:</span>
<span className="ml-2">{new Date(file.created_at * 1000).toLocaleString()}</span>
<span className="font-medium text-gray-600 dark:text-gray-400">
Created:
</span>
<span className="ml-2">
{new Date(file.created_at * 1000).toLocaleString()}
</span>
</div>
<div>
<span className="font-medium text-gray-600 dark:text-gray-400">Content Strategy:</span>
<span className="font-medium text-gray-600 dark:text-gray-400">
Content Strategy:
</span>
<span className="ml-2">{file.chunking_strategy.type}</span>
</div>
</div>
@ -166,9 +201,7 @@ export default function FileDetailPage() {
</div>
</div>
) : (
<p className="text-gray-500 italic text-sm">
File not found.
</p>
<p className="text-gray-500 italic text-sm">File not found.</p>
)}
</CardContent>
</Card>
@ -192,16 +225,27 @@ export default function FileDetailPage() {
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-600 dark:text-gray-400">Content Items:</span>
<span className="font-medium text-gray-600 dark:text-gray-400">
Content Items:
</span>
<span className="ml-2">{contents.content.length}</span>
</div>
<div>
<span className="font-medium text-gray-600 dark:text-gray-400">Total Characters:</span>
<span className="ml-2">{contents.content.reduce((total, item) => total + item.text.length, 0)}</span>
<span className="font-medium text-gray-600 dark:text-gray-400">
Total Characters:
</span>
<span className="ml-2">
{contents.content.reduce(
(total, item) => total + item.text.length,
0
)}
</span>
</div>
</div>
<div className="pt-2">
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Preview:</span>
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
Preview:
</span>
<div className="mt-1 bg-gray-50 dark:bg-gray-800 rounded-md p-3">
<p className="text-sm text-gray-900 dark:text-gray-100 line-clamp-3">
{contents.content[0]?.text.substring(0, 200)}...

View file

@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useParams } from "next/navigation";
import { useAuthClient } from "@/hooks/use-auth-client";
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
@ -11,7 +11,6 @@ export default function VectorStoreDetailPage() {
const params = useParams();
const id = params.id as string;
const client = useAuthClient();
const router = useRouter();
const [store, setStore] = useState<VectorStore | null>(null);
const [files, setFiles] = useState<VectorStoreFile[]>([]);
@ -34,9 +33,7 @@ export default function VectorStoreDetailPage() {
setStore(response as VectorStore);
} catch (err) {
setErrorStore(
err instanceof Error
? err
: new Error("Failed to load vector store."),
err instanceof Error ? err : new Error("Failed to load vector store.")
);
} finally {
setIsLoadingStore(false);
@ -55,18 +52,18 @@ export default function VectorStoreDetailPage() {
setIsLoadingFiles(true);
setErrorFiles(null);
try {
const result = await client.vectorStores.files.list(id as any);
setFiles((result as any).data);
const result = await client.vectorStores.files.list(id);
setFiles((result as { data: VectorStoreFile[] }).data);
} catch (err) {
setErrorFiles(
err instanceof Error ? err : new Error("Failed to load files."),
err instanceof Error ? err : new Error("Failed to load files.")
);
} finally {
setIsLoadingFiles(false);
}
};
fetchFiles();
}, [id]);
}, [id, client.vectorStores.files]);
return (
<VectorStoreDetailView

View file

@ -1,7 +1,6 @@
"use client";
import React from "react";
import { useAuthClient } from "@/hooks/use-auth-client";
import type {
ListVectorStoresResponse,
VectorStore,
@ -12,7 +11,6 @@ import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
@ -21,7 +19,6 @@ import {
import { Skeleton } from "@/components/ui/skeleton";
export default function VectorStoresPage() {
const client = useAuthClient();
const router = useRouter();
const {
data: stores,
@ -37,7 +34,7 @@ export default function VectorStoresPage() {
after: params.after,
limit: params.limit,
order: params.order,
} as any);
} as Parameters<typeof client.vectorStores.list>[0]);
return response as ListVectorStoresResponse;
},
errorMessagePrefix: "vector stores",
@ -53,11 +50,11 @@ export default function VectorStoresPage() {
const renderContent = () => {
if (status === "loading") {
return (
<div className="space-y-2">
<Skeleton className="h-8 w-full"/>
<Skeleton className="h-4 w-full"/>
<Skeleton className="h-4 w-full"/>
</div>
<div className="space-y-2">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
);
}
@ -70,72 +67,72 @@ export default function VectorStoresPage() {
}
return (
<div className="overflow-auto flex-1 min-h-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Created</TableHead>
<TableHead>Completed</TableHead>
<TableHead>Cancelled</TableHead>
<TableHead>Failed</TableHead>
<TableHead>In Progress</TableHead>
<TableHead>Total</TableHead>
<TableHead>Usage Bytes</TableHead>
<TableHead>Provider ID</TableHead>
<TableHead>Provider Vector DB ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stores.map((store) => {
const fileCounts = store.file_counts;
const metadata = store.metadata || {};
const providerId = metadata.provider_id ?? "";
const providerDbId = metadata.provider_vector_db_id ?? "";
<div className="overflow-auto flex-1 min-h-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Created</TableHead>
<TableHead>Completed</TableHead>
<TableHead>Cancelled</TableHead>
<TableHead>Failed</TableHead>
<TableHead>In Progress</TableHead>
<TableHead>Total</TableHead>
<TableHead>Usage Bytes</TableHead>
<TableHead>Provider ID</TableHead>
<TableHead>Provider Vector DB ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stores.map(store => {
const fileCounts = store.file_counts;
const metadata = store.metadata || {};
const providerId = metadata.provider_id ?? "";
const providerDbId = metadata.provider_vector_db_id ?? "";
return (
<TableRow
key={store.id}
onClick={() => router.push(`/logs/vector-stores/${store.id}`)}
className="cursor-pointer hover:bg-muted/50"
return (
<TableRow
key={store.id}
onClick={() => router.push(`/logs/vector-stores/${store.id}`)}
className="cursor-pointer hover:bg-muted/50"
>
<TableCell>
<Button
variant="link"
className="p-0 h-auto font-mono text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
onClick={() =>
router.push(`/logs/vector-stores/${store.id}`)
}
>
<TableCell>
<Button
variant="link"
className="p-0 h-auto font-mono text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
onClick={() =>
router.push(`/logs/vector-stores/${store.id}`)
}
>
{store.id}
</Button>
</TableCell>
<TableCell>{store.name}</TableCell>
<TableCell>
{new Date(store.created_at * 1000).toLocaleString()}
</TableCell>
<TableCell>{fileCounts.completed}</TableCell>
<TableCell>{fileCounts.cancelled}</TableCell>
<TableCell>{fileCounts.failed}</TableCell>
<TableCell>{fileCounts.in_progress}</TableCell>
<TableCell>{fileCounts.total}</TableCell>
<TableCell>{store.usage_bytes}</TableCell>
<TableCell>{providerId}</TableCell>
<TableCell>{providerDbId}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
{store.id}
</Button>
</TableCell>
<TableCell>{store.name}</TableCell>
<TableCell>
{new Date(store.created_at * 1000).toLocaleString()}
</TableCell>
<TableCell>{fileCounts.completed}</TableCell>
<TableCell>{fileCounts.cancelled}</TableCell>
<TableCell>{fileCounts.failed}</TableCell>
<TableCell>{fileCounts.in_progress}</TableCell>
<TableCell>{fileCounts.total}</TableCell>
<TableCell>{store.usage_bytes}</TableCell>
<TableCell>{providerId}</TableCell>
<TableCell>{providerDbId}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
};
return (
<div className="space-y-4">
<h1 className="text-2xl font-semibold">Vector Stores</h1>
{renderContent()}
</div>
<div className="space-y-4">
<h1 className="text-2xl font-semibold">Vector Stores</h1>
{renderContent()}
</div>
);
}