From 573294509fb4dfa3792c06ac433fe32e6826634d Mon Sep 17 00:00:00 2001 From: Trynax Date: Fri, 15 Aug 2025 12:52:19 +0100 Subject: [PATCH 1/7] feat: Add Document model and database schema for artifacts support --- .../migration.sql | 22 +++++++++++++++++++ prisma/schema.prisma | 21 ++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 prisma/migrations/20250815115018_add_documents_artifacts/migration.sql diff --git a/prisma/migrations/20250815115018_add_documents_artifacts/migration.sql b/prisma/migrations/20250815115018_add_documents_artifacts/migration.sql new file mode 100644 index 00000000..d094e98c --- /dev/null +++ b/prisma/migrations/20250815115018_add_documents_artifacts/migration.sql @@ -0,0 +1,22 @@ +-- CreateEnum +CREATE TYPE "ArtifactKind" AS ENUM ('text', 'code', 'custom'); + +-- CreateTable +CREATE TABLE "Document" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT, + "kind" "ArtifactKind" NOT NULL DEFAULT 'text', + "userId" TEXT NOT NULL, + "chatId" TEXT, + + CONSTRAINT "Document_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 153dbc73..c82a95aa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -57,6 +57,7 @@ model User { videos Video[] userFeatures UserFeature[] workbenches Workbench[] + documents Document[] } enum UserRole { @@ -92,6 +93,7 @@ model Chat { messages Message[] stream Stream[] workbench Workbench? @relation(fields: [workbenchId], references: [id], onDelete: SetNull) + documents Document[] } model Message { @@ -190,4 +192,23 @@ model Tool { toolkit Toolkit @relation(fields: [toolkitId], references: [id], onDelete: Cascade) @@id([id, toolkitId]) +} + +enum ArtifactKind { + text + code + custom +} + +model Document { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? @db.Text + kind ArtifactKind @default(text) + userId String + chatId String? // Optional association with a chat + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + chat Chat? @relation(fields: [chatId], references: [id], onDelete: SetNull) } \ No newline at end of file From 45e129ad398b76848c2e7b5a0d73cfc9d8fa8b83 Mon Sep 17 00:00:00 2001 From: Trynax Date: Fri, 15 Aug 2025 14:02:30 +0100 Subject: [PATCH 2/7] feat: Implement basic artifacts system with text, code, and custom types --- src/app/(general)/test-artifacts/page.tsx | 187 +++++++++++++++++++ src/artifacts/code/client.tsx | 183 +++++++++++++++++++ src/artifacts/code/server.ts | 136 ++++++++++++++ src/artifacts/custom/client.tsx | 162 ++++++++++++++++ src/artifacts/custom/server.ts | 94 ++++++++++ src/artifacts/text/client.tsx | 138 ++++++++++++++ src/artifacts/text/server.ts | 77 ++++++++ src/lib/artifacts/artifact.ts | 33 ++++ src/lib/artifacts/index.ts | 3 + src/lib/artifacts/server.ts | 40 ++++ src/lib/artifacts/types.ts | 67 +++++++ src/server/api/root.ts | 2 + src/server/api/routers/documents.ts | 213 ++++++++++++++++++++++ src/server/api/routers/index.ts | 1 + 14 files changed, 1336 insertions(+) create mode 100644 src/app/(general)/test-artifacts/page.tsx create mode 100644 src/artifacts/code/client.tsx create mode 100644 src/artifacts/code/server.ts create mode 100644 src/artifacts/custom/client.tsx create mode 100644 src/artifacts/custom/server.ts create mode 100644 src/artifacts/text/client.tsx create mode 100644 src/artifacts/text/server.ts create mode 100644 src/lib/artifacts/artifact.ts create mode 100644 src/lib/artifacts/index.ts create mode 100644 src/lib/artifacts/server.ts create mode 100644 src/lib/artifacts/types.ts create mode 100644 src/server/api/routers/documents.ts diff --git a/src/app/(general)/test-artifacts/page.tsx b/src/app/(general)/test-artifacts/page.tsx new file mode 100644 index 00000000..a75de459 --- /dev/null +++ b/src/app/(general)/test-artifacts/page.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState } from "react"; +import { api } from "@/trpc/react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu"; +import { ArtifactKind } from "@prisma/client"; +import { ChevronDown } from "lucide-react"; + +export default function ArtifactsTestPage() { + const [title, setTitle] = useState(""); + const [kind, setKind] = useState("text"); + + const { data: documents, refetch } = api.documents.list.useQuery({}); + const createDocument = api.documents.create.useMutation({ + onSuccess: () => { + refetch(); + setTitle(""); + }, + }); + const generateContent = api.documents.generateContent.useMutation({ + onSuccess: () => { + refetch(); + setTitle(""); + }, + }); + const deleteDocument = api.documents.delete.useMutation({ + onSuccess: () => { + refetch(); + }, + }); + + const handleCreateDocument = () => { + if (!title.trim()) return; + + createDocument.mutate({ + title, + kind, + initialContent: "This is initial content for testing.", + }); + }; + + const handleGenerateContent = () => { + if (!title.trim()) return; + + generateContent.mutate({ + title, + kind, + }); + }; + + return ( +
+
+

Artifacts Testing

+

Test the artifacts system functionality

+
+ + {/* Create Document Form */} + + + Create New Document + Test document creation and content generation + + +
+ setTitle(e.target.value)} + className="flex-1" + /> + + + + + + setKind("text")}> + Text + + setKind("code")}> + Code + + setKind("custom")}> + Custom + + + +
+
+ + +
+
+
+ + {/* Documents List */} + + + Documents ({documents?.documents.length || 0}) + Your created artifacts + + + {documents?.documents.length === 0 ? ( +

No documents yet. Create one above!

+ ) : ( +
+ {documents?.documents.map((document) => ( +
+
+
+

{document.title}

+

+ Type: {document.kind} • Created: {new Date(document.createdAt).toLocaleString()} +

+
+ +
+ {document.content && ( +
+
{document.content}
+
+ )} +
+ ))} +
+ )} +
+
+ + {/* API Status */} + + + System Status + + +
+
+ Database:{" "} + Connected ✓ +
+
+ AI Provider:{" "} + Ready ✓ +
+
+ Documents API:{" "} + Working ✓ +
+
+ Artifact Types:{" "} + Text, Code, Custom +
+
+
+
+
+ ); +} diff --git a/src/artifacts/code/client.tsx b/src/artifacts/code/client.tsx new file mode 100644 index 00000000..d00be1fd --- /dev/null +++ b/src/artifacts/code/client.tsx @@ -0,0 +1,183 @@ +import { Artifact } from "@/lib/artifacts/artifact"; +import type { ArtifactMetadata } from "@/lib/artifacts/types"; +import { Play, Copy, Download, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; + +interface CodeArtifactMetadata extends ArtifactMetadata { + language: string; + lineCount: number; + lastExecuted?: Date; + isExecuting: boolean; +} + +export const codeArtifact = new Artifact<"code", CodeArtifactMetadata>({ + kind: "code", + description: "A code artifact for writing, editing, and executing code snippets.", + + initialize: async ({ documentId, setMetadata }) => { + setMetadata({ + language: "javascript", + lineCount: 0, + isExecuting: false, + }); + }, + + onStreamPart: ({ streamPart, setMetadata, setArtifact }) => { + if (streamPart.type === "language-update") { + setMetadata((metadata) => ({ + ...metadata, + language: streamPart.content as string, + })); + } + + if (streamPart.type === "content-update") { + setArtifact((draftArtifact) => { + const newContent = draftArtifact.content + (streamPart.content as string); + const lineCount = newContent.split('\n').length; + + setMetadata((metadata) => ({ + ...metadata, + lineCount, + })); + + return { + ...draftArtifact, + content: newContent, + status: "streaming", + }; + }); + } + }, + + content: ({ + mode, + status, + content, + isCurrentVersion, + currentVersionIndex, + onSaveContent, + getDocumentContentById, + isLoading, + metadata, + }) => { + if (isLoading) { + return ( +
+
+ Loading code artifact... +
+ ); + } + + if (mode === "diff") { + const oldContent = getDocumentContentById(currentVersionIndex - 1); + const newContent = getDocumentContentById(currentVersionIndex); + + return ( +
+

Code Comparison

+
+
+

Previous Version

+
+
+                  {oldContent}
+                
+
+
+
+

Current Version

+
+
+                  {newContent}
+                
+
+
+
+
+ ); + } + + const executeCode = async () => { + // TODO: Implement code execution using E2B or similar service + toast.info("Code execution coming soon!"); + }; + + return ( +
+
+
+ Language: {metadata.language} + Lines: {metadata.lineCount} + {metadata.lastExecuted && ( + Last executed: {metadata.lastExecuted.toLocaleTimeString()} + )} +
+ +
+ +
+
+ +
+
+ Language: {metadata.language} +
+ +