Improve image upload in debug drawer

This commit is contained in:
Xin Wang
2026-08-05 21:28:51 +08:00
parent e36ca308b8
commit 0c0a51da95
6 changed files with 37 additions and 7 deletions

View File

@@ -9,11 +9,14 @@ DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/postgres
# ---- 服务监听 & 跨域 ---- # ---- 服务监听 & 跨域 ----
HOST=0.0.0.0 HOST=0.0.0.0
PORT=8000 PORT=8000
# NLTK 3.9+ import guard vs local .venv under backend/ (set in app.py by default; override here if needed)
# NLTK_DISABLE_IMPORT_SECURITY=1
# 前端开发地址,允许跨域(公网部署时加上实际前端 origin) # 前端开发地址,允许跨域(公网部署时加上实际前端 origin)
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
# ---- RustFS / S3-compatible storage ---- # ---- RustFS / S3-compatible storage ----
S3_ENDPOINT_URL=http://localhost:9000 # Use 127.0.0.1 (not localhost) if HTTP_PROXY is set — proxies often return 502 for localhost.
S3_ENDPOINT_URL=http://127.0.0.1:9000
S3_ACCESS_KEY=rustfsadmin S3_ACCESS_KEY=rustfsadmin
S3_SECRET_KEY=rustfsadmin S3_SECRET_KEY=rustfsadmin
S3_BUCKET=ai-video S3_BUCKET=ai-video

View File

@@ -12,6 +12,12 @@
/api/webrtc/ice-servers WebRTC STUN/TURN 配置 /api/webrtc/ice-servers WebRTC STUN/TURN 配置
""" """
import os
# NLTK 3.9+ blocks dependency imports when .venv lives under cwd (local dev layout).
# pipecat -> nltk -> regex hits this false positive; disable before any nltk import.
os.environ.setdefault("NLTK_DISABLE_IMPORT_SECURITY", "1")
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import settings import settings

View File

@@ -235,6 +235,9 @@ class ConversationRecorder:
}, },
) )
) )
# Flush before artifact insert: db.get() below autoflushes and
# can otherwise write conversation_artifacts first (FK violation).
await db.flush()
db.add( db.add(
ConversationArtifact( ConversationArtifact(
id=artifact_id, id=artifact_id,

View File

@@ -52,7 +52,7 @@ TURN_PASSWORD = os.getenv("TURN_PASSWORD", "")
TURN_CREDENTIAL_TTL = int(os.getenv("TURN_CREDENTIAL_TTL", "86400")) TURN_CREDENTIAL_TTL = int(os.getenv("TURN_CREDENTIAL_TTL", "86400"))
# ---- S3-compatible object storage (RustFS in local compose) ---- # ---- S3-compatible object storage (RustFS in local compose) ----
S3_ENDPOINT_URL = os.getenv("S3_ENDPOINT_URL", "http://localhost:9000") S3_ENDPOINT_URL = os.getenv("S3_ENDPOINT_URL", "http://127.0.0.1:9000")
S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY", "rustfsadmin") S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY", "rustfsadmin")
S3_SECRET_KEY = os.getenv("S3_SECRET_KEY", "rustfsadmin") S3_SECRET_KEY = os.getenv("S3_SECRET_KEY", "rustfsadmin")
S3_BUCKET = os.getenv("S3_BUCKET", "ai-video") S3_BUCKET = os.getenv("S3_BUCKET", "ai-video")

View File

@@ -122,6 +122,9 @@ class ConversationRecorderTest(unittest.IsolatedAsyncioTestCase):
def add(self, value): def add(self, value):
self.added.append(value) self.added.append(value)
async def flush(self):
return None
async def get(self, _model, _session_id): async def get(self, _model, _session_id):
return conversation return conversation

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { import {
AudioLines, AudioLines,
Braces, Braces,
@@ -1636,12 +1636,26 @@ function DebugTranscriptPanel({
recording?: boolean; recording?: boolean;
}) { }) {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
// 新消息时滚到底部 const scrollToBottom = useCallback(() => {
useEffect(() => {
const el = scrollRef.current; const el = scrollRef.current;
if (el) el.scrollTop = el.scrollHeight; if (el) el.scrollTop = el.scrollHeight;
}, [messages]); }, []);
// Scroll when message list changes (before paint).
useLayoutEffect(() => {
scrollToBottom();
}, [messages, scrollToBottom]);
// Images load after the message row mounts; re-scroll when content height grows.
useEffect(() => {
const content = contentRef.current;
if (!content) return;
const observer = new ResizeObserver(() => scrollToBottom());
observer.observe(content);
return () => observer.disconnect();
}, [scrollToBottom]);
if (messages.length === 0) { if (messages.length === 0) {
return ( return (
@@ -1671,7 +1685,7 @@ function DebugTranscriptPanel({
ref={scrollRef} ref={scrollRef}
className="scrollbar-subtle flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4" className="scrollbar-subtle flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4"
> >
<div className="flex flex-col gap-4"> <div ref={contentRef} className="flex flex-col gap-4">
{messages.map((message) => { {messages.map((message) => {
const time = formatMessageTime(message.timestamp); const time = formatMessageTime(message.timestamp);
return message.role === "assistant" ? ( return message.role === "assistant" ? (
@@ -1707,6 +1721,7 @@ function DebugTranscriptPanel({
src={attachment.url} src={attachment.url}
alt={attachment.alt} alt={attachment.alt}
className="max-h-72 w-full rounded-[0.8rem] object-cover" className="max-h-72 w-full rounded-[0.8rem] object-cover"
onLoad={scrollToBottom}
/> />
))} ))}
{message.content && ( {message.content && (