@@ -4,6 +4,7 @@ import { Context } from "hono";
44import { z } from "zod" ;
55import { App , PublishEvent } from "../types.js" ;
66import { generateVibeSlug } from "@vibes.diy/hosting-base" ;
7+ import { callAI , imageGen } from "call-ai" ;
78
89// Variables type for context (was previously in deleted auth middleware)
910interface Variables {
@@ -42,6 +43,102 @@ async function processScreenshot(
4243 }
4344}
4445
46+ function getCallAiApiKey ( env : Env ) : string | undefined {
47+ const typedEnv = env as Env & {
48+ CALLAI_API_KEY ?: string ;
49+ OPENROUTER_API_KEY ?: string ;
50+ SERVER_OPENROUTER_API_KEY ?: string ;
51+ } ;
52+
53+ return (
54+ typedEnv . CALLAI_API_KEY ||
55+ typedEnv . OPENROUTER_API_KEY ||
56+ typedEnv . SERVER_OPENROUTER_API_KEY
57+ ) ;
58+ }
59+
60+ function base64ToArrayBuffer ( base64Data : string ) {
61+ if ( typeof atob === "function" ) {
62+ const binaryData = atob ( base64Data ) ;
63+ const len = binaryData . length ;
64+ const bytes = new Uint8Array ( len ) ;
65+ for ( let i = 0 ; i < len ; i ++ ) {
66+ bytes [ i ] = binaryData . charCodeAt ( i ) ;
67+ }
68+ return bytes . buffer ;
69+ }
70+
71+ const buffer = Buffer . from ( base64Data , "base64" ) ;
72+ return buffer . buffer . slice ( buffer . byteOffset , buffer . byteOffset + buffer . byteLength ) ;
73+ }
74+
75+ async function generateAppSummary (
76+ app : z . infer < typeof App > ,
77+ apiKey ?: string ,
78+ ) : Promise < string | null > {
79+ if ( ! apiKey ) {
80+ console . warn ( "⚠️ CALLAI_API_KEY not set - skipping app summary generation" ) ;
81+ return null ;
82+ }
83+
84+ try {
85+ const contextPieces = [
86+ app . title ? `Title: ${ app . title } ` : null ,
87+ app . prompt ? `Prompt: ${ app . prompt } ` : null ,
88+ app . code ? `Code snippet: ${ app . code . slice ( 0 , 1000 ) } ` : null ,
89+ ] . filter ( Boolean ) ;
90+
91+ const messages = [
92+ {
93+ role : "system" as const ,
94+ content :
95+ "You write concise, single-sentence summaries of small web apps. Keep it under 35 words, highlight the main category and what the app helps users do." ,
96+ } ,
97+ {
98+ role : "user" as const ,
99+ content : contextPieces . join ( "\n\n" ) ,
100+ } ,
101+ ] ;
102+
103+ const response = await callAI ( messages , { apiKey } ) ;
104+ return typeof response === "string" ? response . trim ( ) : null ;
105+ } catch ( error ) {
106+ console . error ( "Error generating app summary:" , error ) ;
107+ return null ;
108+ }
109+ }
110+
111+ async function generateAppIcon (
112+ app : z . infer < typeof App > ,
113+ kv : KVNamespace ,
114+ apiKey ?: string ,
115+ ) : Promise < string | null > {
116+ if ( ! apiKey ) {
117+ console . warn ( "⚠️ CALLAI_API_KEY not set - skipping app icon generation" ) ;
118+ return null ;
119+ }
120+
121+ try {
122+ const category = app . title || app . name || "app" ;
123+ const prompt = `Minimal black icon on a white background, enclosed in a circle, representing ${ category } . Use clear, text-free imagery to convey the category. Avoid letters or numbers.` ;
124+ const response = await imageGen ( prompt , { apiKey, size : "512x512" } ) ;
125+ const iconBase64 = response . data ?. [ 0 ] ?. b64_json ;
126+
127+ if ( ! iconBase64 ) {
128+ console . warn ( "⚠️ No icon data returned from imageGen" ) ;
129+ return null ;
130+ }
131+
132+ const iconArrayBuffer = base64ToArrayBuffer ( iconBase64 ) ;
133+ const iconKey = `${ app . slug } -icon` ;
134+ await kv . put ( iconKey , iconArrayBuffer ) ;
135+ return iconKey ;
136+ } catch ( error ) {
137+ console . error ( "Error generating app icon:" , error ) ;
138+ return null ;
139+ }
140+ }
141+
45142// Request body schema for app creation
46143const AppCreateRequestSchema = z . object ( {
47144 chatId : z . string ( ) ,
@@ -102,6 +199,7 @@ export class AppCreate extends OpenAPIRoute {
102199
103200 // Get the KV namespace from the context
104201 const kv = c . env . KV ;
202+ const callAiApiKey = getCallAiApiKey ( c . env ) ;
105203
106204 // Check if the app with this chatId already exists
107205 const existingApp = await kv . get ( app . chatId ) ;
@@ -210,6 +308,9 @@ export class AppCreate extends OpenAPIRoute {
210308 title : app . title || `App ${ slug } ` ,
211309 remixOf : app . remixOf === undefined ? null : app . remixOf ,
212310 hasScreenshot : false ,
311+ summary : null ,
312+ iconKey : null ,
313+ hasIcon : false ,
213314 shareToFirehose : app . shareToFirehose ,
214315 customDomain : app . customDomain || null ,
215316 } ;
@@ -231,6 +332,21 @@ export class AppCreate extends OpenAPIRoute {
231332 savedApp = appToSave ;
232333 }
233334
335+ const summary = await generateAppSummary ( savedApp , callAiApiKey ) ;
336+ if ( summary ) {
337+ savedApp . summary = summary ;
338+ }
339+
340+ const iconKey = await generateAppIcon ( savedApp , kv , callAiApiKey ) ;
341+ if ( iconKey ) {
342+ savedApp . iconKey = iconKey ;
343+ savedApp . hasIcon = true ;
344+ }
345+
346+ // Persist any AI-enriched fields
347+ await kv . put ( savedApp . chatId , JSON . stringify ( savedApp ) ) ;
348+ await kv . put ( savedApp . slug , JSON . stringify ( savedApp ) ) ;
349+
234350 // Send event to queue for processing
235351 try {
236352 if ( ! c . env . PUBLISH_QUEUE ) {
0 commit comments