Skip to content
Open
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
40 changes: 29 additions & 11 deletions c1-artifact/src/app/api/ask/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@ import OpenAI from "openai";
import { NextRequest } from "next/server";
import { makeC1Response } from "@thesysai/genui-sdk/server";
import { transformStream } from "@crayonai/stream";
import { saveVersion } from "../../../lib/versionStore";

export async function POST(req: NextRequest) {
const apiKey = process.env.THESYS_API_KEY;
if (!apiKey) {
return new Response("Missing THESYS_API_KEY", { status: 500 });
}

const { prompt, artifactType, artifactId, artifactContent } = (await req.json()) as {
prompt?: string;
artifactType?: "slides" | "report";
artifactId?: string;
artifactContent?: string;
};
const { prompt, artifactType, artifactId, artifactContent } =
(await req.json()) as {
prompt?: string;
artifactType?: "slides" | "report";
artifactId?: string;
artifactContent?: string;
};

if (!prompt || typeof prompt !== "string") {
return new Response("Missing 'prompt'", { status: 400 });
}

if (!artifactId) {
return new Response("Missing 'artifactId'", { status: 400 });
}

const client = new OpenAI({
apiKey,
baseURL: "https://api.thesys.dev/v1/artifact",
Expand All @@ -29,9 +35,12 @@ export async function POST(req: NextRequest) {
role: "system" | "user" | "assistant";
content: string;
}> = [];

// Include previous artifact content if editing
if (typeof artifactContent === "string" && artifactContent.trim().length > 0) {
if (
typeof artifactContent === "string" &&
artifactContent.trim().length > 0
) {
messages.push({ role: "assistant", content: artifactContent });
}
messages.push({ role: "user", content: prompt });
Expand All @@ -47,7 +56,7 @@ export async function POST(req: NextRequest) {
id: artifactId,
c1_artifact_type: artifactType,
}),
}
},
});

const c1Response = makeC1Response();
Expand All @@ -57,11 +66,15 @@ export async function POST(req: NextRequest) {
isAborted = true;
});

// Accumulate content to save as a version
let accumulatedContent = "";

transformStream(
stream,
(chunk) => {
const content = chunk.choices?.[0]?.delta?.content;
if (content) {
accumulatedContent += content;
c1Response.writeContent(content);
}
},
Expand All @@ -74,8 +87,12 @@ export async function POST(req: NextRequest) {
return;
}
c1Response.end();
// if you want to store the response just call
// c1Response.getAssistantMessage() to get the assistant message

// Save the completed artifact as a new version
if (accumulatedContent) {
const version = saveVersion(artifactId, accumulatedContent, prompt);
console.log(`Saved version ${version.id} for artifact ${artifactId}`);
}
},
}
);
Expand All @@ -84,6 +101,7 @@ export async function POST(req: NextRequest) {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
"X-Artifact-Id": artifactId,
},
});
}
28 changes: 28 additions & 0 deletions c1-artifact/src/app/api/versions/[artifactId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from "next/server";
import { getVersions, getVersion } from "../../../../lib/versionStore";

export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ artifactId: string }> }
) {
const { artifactId } = await params;

// Check if a specific version is requested
const versionId = req.nextUrl.searchParams.get("versionId");

if (versionId) {
const version = getVersion(artifactId, parseInt(versionId, 10));
if (!version) {
return NextResponse.json(
{ error: "Version not found" },
{ status: 404 }
);
}
return NextResponse.json(version);
}

// Return all versions for the artifact
const versions = getVersions(artifactId);
return NextResponse.json({ artifactId, versions });
}

6 changes: 6 additions & 0 deletions c1-artifact/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default function Home() {
send,
stop,
clear,
versions,
currentVersionIndex,
selectVersion,
} = useArtifactStream();

const suggestions = [
Expand All @@ -38,6 +41,9 @@ export default function Home() {
artifactType={artifactType}
onClear={clear}
isLoading={isLoading}
versions={versions}
currentVersionIndex={currentVersionIndex}
onSelectVersion={selectVersion}
/>

<Suggestions items={suggestions} onSelect={(s) => send(s)} />
Expand Down
26 changes: 26 additions & 0 deletions c1-artifact/src/components/OutputPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
import { C1Component, ThemeProvider } from "@thesysai/genui-sdk";
import type { Version } from "../hooks/useArtifactStream";

type OutputPanelProps = {
artifact: string;
artifactType: "slides" | "report";
onClear: () => void;
isLoading: boolean;
versions: Version[];
currentVersionIndex: number;
onSelectVersion: (index: number) => void;
};

export function OutputPanel({
artifact,
artifactType,
onClear,
isLoading,
versions,
currentVersionIndex,
onSelectVersion,
}: OutputPanelProps) {
return (
<section className="relative rounded-2xl border border-white/10 bg-neutral-900/40 backdrop-blur p-4 shadow-2xl min-h-[244px]">
<div className="flex items-center justify-between mb-2">
<div className="text-xs text-neutral-400">{artifactType === "slides" ? "Slides" : "Report"} Output</div>
<div className="flex items-center gap-2">
{versions.length > 0 && (
<select
value={currentVersionIndex}
onChange={(e) => onSelectVersion(parseInt(e.target.value, 10))}
disabled={isLoading}
className="px-2 h-8 rounded-md border border-white/10 bg-white/5 text-xs text-neutral-100 disabled:opacity-40 cursor-pointer appearance-none pr-6 bg-no-repeat bg-right"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M3 4.5L6 7.5L9 4.5'/%3E%3C/svg%3E")`,
backgroundPosition: "right 6px center",
}}
>
{versions.map((version, index) => (
<option key={version.id} value={index} className="bg-neutral-800">
Version {version.id}
</option>
))}
</select>
)}
<button
onClick={onClear}
disabled={!artifact}
Expand Down
51 changes: 50 additions & 1 deletion c1-artifact/src/hooks/useArtifactStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,58 @@ import { useCallback, useRef, useState } from "react";

export type ArtifactType = "slides" | "report";

export interface Version {
id: number;
content: string;
prompt: string;
timestamp: number;
}

export function useArtifactStream() {
const [prompt, setPrompt] = useState("");
const [artifact, setArtifact] = useState("");
const [artifactType, setArtifactType] = useState<ArtifactType>("slides");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [versions, setVersions] = useState<Version[]>([]);
const [currentVersionIndex, setCurrentVersionIndex] = useState<number>(-1);

const abortRef = useRef<AbortController | null>(null);
const previousArtifactRef = useRef<string>("");
const artifactIdRef = useRef<string>("");
const currentArtifactTypeRef = useRef<ArtifactType>("slides");

const fetchVersions = useCallback(async (artifactId: string) => {
try {
const res = await fetch(`/api/versions/${encodeURIComponent(artifactId)}`);
if (res.ok) {
const data = await res.json();
setVersions(data.versions || []);
// Set current version to the latest
if (data.versions && data.versions.length > 0) {
setCurrentVersionIndex(data.versions.length - 1);
}
}
} catch (err) {
console.error("Failed to fetch versions:", err);
}
}, []);

const selectVersion = useCallback((index: number) => {
if (index >= 0 && index < versions.length) {
setCurrentVersionIndex(index);
setArtifact(versions[index].content);
}
}, [versions]);

const changeArtifactType = useCallback((newType: ArtifactType) => {
setArtifactType(newType);
// Clear artifact when switching artifact type
setArtifact("");
artifactIdRef.current = "";
currentArtifactTypeRef.current = newType;
setVersions([]);
setCurrentVersionIndex(-1);
}, []);

const send = useCallback(
Expand All @@ -41,6 +75,8 @@ export function useArtifactStream() {
if (!artifact) {
artifactIdRef.current = `artifact-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
currentArtifactTypeRef.current = artifactType;
setVersions([]);
setCurrentVersionIndex(-1);
}

try {
Expand Down Expand Up @@ -74,6 +110,14 @@ export function useArtifactStream() {

accumulated += decoder.decode();
setArtifact(accumulated);

// Fetch updated versions after generation completes
if (artifactIdRef.current) {
// Small delay to ensure the server has saved the version
setTimeout(() => {
fetchVersions(artifactIdRef.current);
}, 100);
}
} catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") {
setArtifact(previousArtifactRef.current);
Expand All @@ -89,7 +133,7 @@ export function useArtifactStream() {
abortRef.current = null;
}
},
[prompt, artifact, artifactType, isLoading]
[prompt, artifact, artifactType, isLoading, fetchVersions]
);

const stop = useCallback(() => {
Expand All @@ -104,6 +148,8 @@ export function useArtifactStream() {
setArtifact("");
artifactIdRef.current = "";
setError(null);
setVersions([]);
setCurrentVersionIndex(-1);
}, []);

return {
Expand All @@ -117,5 +163,8 @@ export function useArtifactStream() {
send,
stop,
clear,
versions,
currentVersionIndex,
selectVersion,
} as const;
}
Loading