Skip to content

Commit 8bcd8e4

Browse files
bidahclaude
andcommitted
🐛 fix(x-bot): save chat messages, fix images, clean reply format
- Save user + assistant messages to DB in generate route so chat panel shows history when users visit x-bot created projects - Pass tweet image blob URLs as imageAttachments (not base64 images) so they flow through --image-urls to the executor - Simplify reply tweet format: no extra @mentions, just app name + links Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d83b0d commit 8bcd8e4

2 files changed

Lines changed: 74 additions & 32 deletions

File tree

‎apps/web/app/(app)/api/x-bot/generate/route.ts‎

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { NextRequest, NextResponse } from 'next/server'
2-
import { db } from '@/lib/db'
2+
import { db, saveProjectMessages } from '@/lib/db'
33
import { xBotReplies, projects } from '@react-native-vibe-code/database'
44
import { eq } from 'drizzle-orm'
55
import { handleClaudeCodeGeneration } from '@/lib/claude-code-handler'
6-
import { blobUrlsToBase64 } from '@/lib/x-bot/extract-images'
76
import { getAuthClient } from '@/lib/x-bot/process-mention'
7+
import type { UIMessage } from 'ai'
88

99
export const maxDuration = 300 // 5 minutes
1010

@@ -71,40 +71,73 @@ export async function POST(request: NextRequest) {
7171
.set({ generationStatus: 'generating' })
7272
.where(eq(xBotReplies.tweetId, tweetId))
7373

74-
// Convert blob URLs to base64 for AI
75-
let images: string[] = []
76-
if (imageUrls && imageUrls.length > 0) {
77-
console.log(`[X-Bot Generate] Converting ${imageUrls.length} images to base64`)
78-
images = await blobUrlsToBase64(imageUrls)
79-
}
74+
// Build imageAttachments from blob URLs so they flow through to the executor
75+
// (the executor downloads images via --image-urls, base64 `images` field is not used)
76+
const imageAttachments = (imageUrls && imageUrls.length > 0)
77+
? imageUrls.map((url, i) => ({
78+
url,
79+
contentType: 'image/png',
80+
name: `tweet-image-${i + 1}.png`,
81+
size: 0,
82+
}))
83+
: undefined
8084

8185
// Build the prompt
82-
const prompt = buildPrompt(appDescription, images.length > 0)
86+
const prompt = buildPrompt(appDescription, !!imageAttachments)
8387

8488
// Track generation result
8589
let generationSucceeded = false
8690
let generationError: string | null = null
91+
let fullContent = ''
8792

8893
// Call the Claude Code handler
8994
await handleClaudeCodeGeneration(
9095
{
9196
projectId,
9297
userID: userId,
9398
userMessage: prompt,
94-
images,
99+
imageAttachments,
95100
isFirstMessage: true,
96101
sandboxId,
97102
messageId: `xbot-${tweetId}`,
98103
},
99104
{
100105
onMessage: (message) => {
101-
// Log progress (we can't stream to Twitter, so just log)
106+
// Accumulate content for saving to DB
107+
fullContent += message + '\n'
102108
console.log(`[X-Bot Generate] Progress: ${message.substring(0, 100)}...`)
103109
},
104110
onComplete: async (result) => {
105111
console.log(`[X-Bot Generate] Generation complete for tweet ${tweetId}`)
106112
generationSucceeded = true
107113

114+
// Add summary to content
115+
if (result.summary) {
116+
fullContent += `\n\n✅ ${result.summary}`
117+
}
118+
119+
// Save messages to DB so chat panel shows history
120+
try {
121+
const userMessage: UIMessage = {
122+
id: `xbot-${tweetId}`,
123+
role: 'user',
124+
content: appDescription,
125+
createdAt: new Date(),
126+
parts: [{ type: 'text', text: appDescription }],
127+
}
128+
const assistantMessage: UIMessage = {
129+
id: `xbot-${tweetId}-response`,
130+
role: 'assistant',
131+
content: fullContent,
132+
createdAt: new Date(),
133+
parts: [{ type: 'text', text: fullContent }],
134+
}
135+
await saveProjectMessages(projectId, userId, [userMessage, assistantMessage] as any)
136+
console.log(`[X-Bot Generate] Messages saved to DB for project ${projectId}`)
137+
} catch (saveError) {
138+
console.error(`[X-Bot Generate] Failed to save messages to DB:`, saveError)
139+
}
140+
108141
// Update xBotReplies with completion
109142
await db
110143
.update(xBotReplies)
@@ -130,6 +163,30 @@ export async function POST(request: NextRequest) {
130163
generationError = error
131164
generationSucceeded = false
132165

166+
// Save partial content + error to DB so user sees what happened
167+
if (fullContent) {
168+
try {
169+
const userMessage: UIMessage = {
170+
id: `xbot-${tweetId}`,
171+
role: 'user',
172+
content: appDescription,
173+
createdAt: new Date(),
174+
parts: [{ type: 'text', text: appDescription }],
175+
}
176+
const assistantMessage: UIMessage = {
177+
id: `xbot-${tweetId}-response`,
178+
role: 'assistant',
179+
content: fullContent + `\n\n❌ Error: ${error}`,
180+
createdAt: new Date(),
181+
parts: [{ type: 'text', text: fullContent + `\n\n❌ Error: ${error}` }],
182+
}
183+
await saveProjectMessages(projectId, userId, [userMessage, assistantMessage] as any)
184+
console.log(`[X-Bot Generate] Partial messages saved to DB for project ${projectId}`)
185+
} catch (saveError) {
186+
console.error(`[X-Bot Generate] Failed to save partial messages:`, saveError)
187+
}
188+
}
189+
133190
// Update xBotReplies with failure
134191
await db
135192
.update(xBotReplies)

‎apps/web/app/(app)/api/x-bot/reply/route.ts‎

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ interface ReplyRequest {
1515

1616
/**
1717
* Build the final reply text within Twitter's 280 character limit.
18-
* Includes app title, brief description, edit link, and remix link.
18+
* Format:
19+
* Your app "title" is ready:
20+
* Edit your app: URL
21+
* Remix it: URL
1922
*/
2023
function buildReplyText(
2124
title: string,
@@ -27,26 +30,8 @@ function buildReplyText(
2730
const editUrl = `${baseUrl}/p/${projectId}`
2831
const remixUrl = `${baseUrl}/p/${projectId}/remix`
2932

30-
const links = `\nEdit your app: ${editUrl}\nRemix it: ${remixUrl}`
31-
const header = `Your app "${title}" is ready!`
32-
33-
// Calculate remaining space for description
34-
// 280 char limit, account for header + links + newlines
35-
const fixedLength = header.length + links.length + 2 // 2 for newlines between sections
36-
const maxDescLength = 280 - fixedLength
37-
38-
if (!appDescription || maxDescLength <= 0) {
39-
const text = `${header}${links}`
40-
return text.length <= 280 ? text : text.substring(0, 280)
41-
}
42-
43-
const desc =
44-
appDescription.length > maxDescLength
45-
? appDescription.substring(0, maxDescLength - 3) + '...'
46-
: appDescription
47-
48-
const text = `${header}\n\n${desc}${links}`
49-
return text.length <= 280 ? text : `${header}${links}`
33+
const text = `Your app "${title}" is ready:\nEdit your app: ${editUrl}\nRemix it: ${remixUrl}`
34+
return text.length <= 280 ? text : text.substring(0, 280)
5035
}
5136

5237
export async function POST(request: NextRequest) {

0 commit comments

Comments
 (0)