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
6 changes: 5 additions & 1 deletion examples/chat/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ MONGODB_URI=mongodb://localhost:27017/provenancekit-chat
# ── AI Providers ──────────────────────────────────────────────────────────────
# OpenAI (Required — primary provider, powers GPT-4o by default)
OPENAI_API_KEY=sk-proj-...
# Optional image-generation model overrides. Defaults follow the current
# OpenAI Images API guide: GPT Image models return base64 PNG data by default.
# OPENAI_IMAGE_MODEL=gpt-image-2
# OPENAI_IMAGE_FALLBACK_MODEL=gpt-image-1

# Anthropic (Optional — enables Claude models in the model selector)
ANTHROPIC_API_KEY=sk-ant-...
Expand Down Expand Up @@ -49,4 +53,4 @@ NEXT_PUBLIC_PK_DASHBOARD_URL=http://localhost:3000

# Base URL used when generating shareable provenance links (the /p/:shareId viewer)
# In production this should point to your deployed provenancekit-app instance
NEXT_PUBLIC_SHARE_BASE_URL=http://localhost:3000
NEXT_PUBLIC_SHARE_BASE_URL=http://localhost:3000
2 changes: 1 addition & 1 deletion examples/chat/app/(app)/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function ChatHomePage() {
</h1>
<p className="text-sm text-muted-foreground">
{authenticated
? "Every response is provenance-tracked. Ask me anything — or try DALL-E, TTS, or voice input."
? "Every response is provenance-tracked. Ask me anything — or try image generation, TTS, or voice input."
: "AI chat with built-in provenance tracking. Sign in to get started."}
</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion examples/chat/app/(app)/provenance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export default function ProvenanceExplorerPage() {
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wide">
Search by file
</p>
<FileSearchPanel />
<FileSearchPanel userId={userId} />
</div>

{/* Stats row */}
Expand Down
133 changes: 97 additions & 36 deletions examples/chat/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Chat API route — streaming AI chat with OpenAI tool calling.
*
* Tools available to the AI:
* • generate_image — DALL-E 3 image generation
* • generate_image — GPT Image generation
* • text_to_speech — OpenAI TTS (tts-1)
* • web_search — simulated (returns instruction for AI to answer from knowledge)
*
Expand Down Expand Up @@ -48,18 +48,69 @@ function getAIProvider(provider: SupportedProvider, model: string) {
}
}

const IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL ?? "gpt-image-1";
const IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL ?? "gpt-image-2";
const FALLBACK_IMAGE_MODEL = process.env.OPENAI_IMAGE_FALLBACK_MODEL ?? "gpt-image-1";

function isGptImageModel(model: string) {
return model.startsWith("gpt-image");
}

function normalizeImageSize(model: string, size: string) {
if (!model.startsWith("gpt-image")) return size;
if (size === "1792x1024") return "1536x1024";
if (size === "1024x1792") return "1024x1536";
if (!isGptImageModel(model)) return size;
return size;
}

function normalizeImageQuality(model: string, quality: string) {
if (!model.startsWith("gpt-image")) return quality;
return quality === "hd" ? "high" : "medium";
if (!isGptImageModel(model)) return quality;
return quality;
}

function normalizeLegacyImageSize(size: string) {
if (size === "1536x1024") return "1792x1024";
if (size === "1024x1536") return "1024x1792";
if (size === "auto") return "1024x1024";
return size;
}

function normalizeLegacyImageQuality(quality: string) {
if (quality === "high") return "hd";
return "standard";
}

function buildImageRequest({
model,
prompt,
size,
quality,
}: {
model: string;
prompt: string;
size: string;
quality: string;
}) {
if (!isGptImageModel(model)) {
return {
model,
prompt,
size: normalizeLegacyImageSize(size),
quality: normalizeLegacyImageQuality(quality),
style: "vivid",
response_format: "url",
n: 1,
};
}

const imageRequest: Record<string, unknown> = {
model,
prompt,
size: normalizeImageSize(model, size),
quality: normalizeImageQuality(model, quality),
output_format: "png",
moderation: "auto",
n: 1,
};

return imageRequest;
}

async function blobFromImageResult(imageUrl: string, b64Json?: string): Promise<Blob | undefined> {
Expand Down Expand Up @@ -313,7 +364,7 @@ export async function POST(req: Request) {
// Track tool results during streaming for provenance recording.
// `blob` holds the pre-downloaded image binary so we can upload it to IPFS
// for real vector embeddings. Downloaded immediately inside the tool execute
// while the DALL-E URL is guaranteed fresh.
// while any generated-image URL or base64 payload is fresh.
const toolResults: Array<{ name: string; result: unknown; blob?: Blob }> = [];

// Capture user message text
Expand All @@ -336,48 +387,58 @@ export async function POST(req: Request) {
tools: {
generate_image: tool({
description:
"Generate a high-quality image using DALL-E 3. Use this when the user asks to create, draw, generate, or visualize an image.",
"Generate an image using OpenAI GPT Image. Use this when the user asks to create, draw, generate, or visualize an image.",
parameters: z.object({
prompt: z.string().describe("Detailed description of the image to generate"),
size: z
.enum(["1024x1024", "1792x1024", "1024x1792"])
.enum(["auto", "1024x1024", "1536x1024", "1024x1536"])
.optional()
.default("1024x1024")
.describe("Image dimensions. Wide/tall sizes are mapped to the active image model's closest supported size."),
.default("auto")
.describe("Image dimensions. Use auto unless the user asks for square, landscape, or portrait."),
quality: z
.enum(["standard", "hd"])
.optional()
.default("standard")
.describe("Image quality"),
style: z
.enum(["vivid", "natural"])
.enum(["auto", "low", "medium", "high"])
.optional()
.default("vivid")
.describe("vivid=dramatic/hyper-real, natural=more subdued"),
.default("auto")
.describe("Rendering quality. Use low for drafts, medium/high for final assets, or auto by default."),
}),
execute: async ({ prompt, size, quality, style }) => {
execute: async ({ prompt, size, quality }) => {
try {
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is not configured.");
}

const openai = getOpenAIClient();
const imageModel = IMAGE_MODEL;
const imageSize = normalizeImageSize(imageModel, size ?? "1024x1024");
const imageQuality = normalizeImageQuality(imageModel, quality ?? "standard");
const imageRequest: Record<string, unknown> = {
model: imageModel,
prompt,
size: imageSize,
quality: imageQuality,
n: 1,
};
if (!imageModel.startsWith("gpt-image")) {
imageRequest.style = style ?? "vivid";
imageRequest.response_format = "url";
const requestedSize = size ?? "auto";
const requestedQuality = quality ?? "auto";
let imageModel = IMAGE_MODEL;
let response;

try {
response = await openai.images.generate(
buildImageRequest({
model: imageModel,
prompt,
size: requestedSize,
quality: requestedQuality,
}) as never
);
} catch (primaryErr) {
if (imageModel === FALLBACK_IMAGE_MODEL) throw primaryErr;
console.warn(
`[chat] image generation failed with ${imageModel}; retrying with ${FALLBACK_IMAGE_MODEL}:`,
primaryErr instanceof Error ? primaryErr.message : primaryErr
);
imageModel = FALLBACK_IMAGE_MODEL;
response = await openai.images.generate(
buildImageRequest({
model: imageModel,
prompt,
size: requestedSize,
quality: requestedQuality,
}) as never
);
}

const response = await openai.images.generate(imageRequest as never);
const firstImage = response.data?.[0] as { url?: string; b64_json?: string; revised_prompt?: string } | undefined;
const b64Json = firstImage?.b64_json;
const imageUrl = firstImage?.url ?? (b64Json ? `data:image/png;base64,${b64Json}` : "");
Expand All @@ -387,7 +448,7 @@ export async function POST(req: Request) {
throw new Error("Image API returned no image data.");
}

// Download the image binary while the URL is fresh (just returned by DALL-E).
// Download the image binary while the generated payload is fresh.
// Storing it here avoids re-fetching later when the URL may have expired,
// and lets the provenance recorder upload the real image to IPFS for embeddings.
let imageBlob: Blob | undefined;
Expand Down
85 changes: 84 additions & 1 deletion examples/chat/components/chat/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ function AttachmentChip({
const governance = attachment.governance ?? defaultAttachmentGovernance("own-original");

return (
<div className="flex flex-col rounded-lg border border-border bg-muted/50 px-2 py-1.5 text-xs max-w-[220px]">
<div className="flex flex-col rounded-lg border border-border bg-muted/50 px-2 py-1.5 text-xs w-[280px] max-w-full">
<div className="flex items-center gap-1.5">
{isImage
? <ImageIcon className="h-3 w-3 shrink-0 text-blue-500" />
Expand Down Expand Up @@ -413,6 +413,89 @@ function AttachmentChip({
<option value="licensed-source">Licensed source</option>
<option value="restricted-source">Restricted source</option>
</select>
<div className="mt-1 grid grid-cols-2 gap-1">
<label className="space-y-0.5">
<span className="text-[9px] font-medium uppercase tracking-wide text-muted-foreground">License</span>
<select
value={governance.licenseType}
onChange={(e) => onGovernanceChange?.({ ...governance, licenseType: e.target.value })}
className="h-6 w-full rounded border border-border bg-background px-1 text-[10px] text-muted-foreground outline-none"
>
<option value="CC-BY-4.0">CC BY 4.0</option>
<option value="CC0-1.0">CC0</option>
<option value="MIT">MIT</option>
<option value="Proprietary">Proprietary</option>
<option value="Custom">Custom</option>
</select>
</label>
<label className="space-y-0.5">
<span className="text-[9px] font-medium uppercase tracking-wide text-muted-foreground">Authorization</span>
<select
value={governance.authorizationStatus}
onChange={(e) =>
onGovernanceChange?.({
...governance,
authorizationStatus: e.target.value as "authorized" | "pending" | "unauthorized",
})
}
className="h-6 w-full rounded border border-border bg-background px-1 text-[10px] text-muted-foreground outline-none"
>
<option value="authorized">Authorized</option>
<option value="pending">Pending</option>
<option value="unauthorized">Unauthorized</option>
</select>
</label>
<label className="space-y-0.5">
<span className="text-[9px] font-medium uppercase tracking-wide text-muted-foreground">AI training</span>
<select
value={governance.aiTraining}
onChange={(e) =>
onGovernanceChange?.({
...governance,
aiTraining: e.target.value as "permitted" | "reserved" | "unspecified",
})
}
className="h-6 w-full rounded border border-border bg-background px-1 text-[10px] text-muted-foreground outline-none"
>
<option value="reserved">Reserved</option>
<option value="permitted">Permitted</option>
<option value="unspecified">Unspecified</option>
</select>
</label>
<label className="space-y-0.5">
<span className="text-[9px] font-medium uppercase tracking-wide text-muted-foreground">Weight</span>
<input
type="number"
min={0}
max={10000}
step={100}
value={governance.contributionWeight}
onChange={(e) =>
onGovernanceChange?.({
...governance,
contributionWeight: Math.max(0, Math.min(10000, Number(e.target.value) || 0)),
})
}
className="h-6 w-full rounded border border-border bg-background px-1 text-[10px] text-muted-foreground outline-none"
/>
</label>
</div>
<input
value={governance.scope}
onChange={(e) => onGovernanceChange?.({ ...governance, scope: e.target.value })}
placeholder="Authorized scope"
className="mt-1 h-6 rounded border border-border bg-background px-1 text-[10px] text-muted-foreground outline-none"
/>
<div className="mt-1 flex items-center gap-1 text-[10px] text-muted-foreground">
{governance.authorizationStatus === "authorized" ? (
<ShieldCheck className="h-2.5 w-2.5 text-emerald-500" />
) : (
<ShieldOff className="h-2.5 w-2.5 text-amber-500" />
)}
<span className="truncate">
{governance.licenseType} · AI training {governance.aiTraining}
</span>
</div>
</div>
);
}
Loading
Loading