fix: hide partial tags in AI Assist and add status feedback UI

- Refined regex to hide partial [AGENT] and [PREVIEW] tags during streaming
- Added 'Neural Link Thinking' indicator for initial response phase
- Added dynamic status badges (e.g., 'Generating web artifact...') during stream
- Improved robustness of streaming content parser
This commit is contained in:
Gemini AI
2025-12-29 02:08:04 +04:00
Unverified
parent 242f718885
commit 0113ba7624

View File

@@ -144,12 +144,25 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
LiveCanvas.displayName = "LiveCanvas"; LiveCanvas.displayName = "LiveCanvas";
const ThinkingIndicator = () => (
<div className="flex items-center gap-1.5 px-4 py-3 bg-white dark:bg-[#0f1a1a]/80 border border-blue-100/70 dark:border-blue-900/50 rounded-2xl rounded-tl-none shadow-sm backdrop-blur-xl animate-in fade-in duration-300">
<div className="flex gap-1">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.3s]" />
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.15s]" />
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce" />
</div>
<span className="text-[10px] font-black text-blue-700/60 dark:text-blue-200/60 uppercase tracking-widest ml-2">Neural Link Thinking...</span>
</div>
);
// --- Helper Functions --- // --- Helper Functions ---
function parseStreamingContent(text: string) { function parseStreamingContent(text: string) {
let agent = "general"; let agent = "general";
let preview: PreviewData | null = null; let preview: PreviewData | null = null;
let chatDisplay = text.trim(); let chatDisplay = text.trim();
let status: string | null = null;
const decodeHtml = (value: string) => value const decodeHtml = (value: string) => value
.replace(/&lt;/g, "<") .replace(/&lt;/g, "<")
.replace(/&gt;/g, ">") .replace(/&gt;/g, ">")
@@ -161,27 +174,6 @@ function parseStreamingContent(text: string) {
return fenced ? fenced[1].trim() : value.trim(); return fenced ? fenced[1].trim() : value.trim();
}; };
const jsonCandidate = text.trim();
if (jsonCandidate.startsWith("{") && jsonCandidate.endsWith("}")) {
try {
const parsed = JSON.parse(jsonCandidate);
if (parsed?.agent) agent = parsed.agent;
if (parsed?.preview?.data) {
preview = {
type: parsed.preview.type || "web",
language: parsed.preview.language || "text",
data: parsed.preview.data,
isStreaming: !text.includes("[/PREVIEW]")
};
}
if (typeof parsed?.content === "string") {
chatDisplay = parsed.content.trim();
}
} catch {
// Ignore malformed JSON during stream
}
}
const agentMatch = text.match(/\[AGENT:([\w-]+)\]/); const agentMatch = text.match(/\[AGENT:([\w-]+)\]/);
if (agentMatch) agent = agentMatch[1]; if (agentMatch) agent = agentMatch[1];
@@ -193,14 +185,17 @@ function parseStreamingContent(text: string) {
data: previewMatch[3].trim(), data: previewMatch[3].trim(),
isStreaming: !text.includes("[/PREVIEW]") isStreaming: !text.includes("[/PREVIEW]")
}; };
if (preview.isStreaming) {
status = `Generating ${preview.type} artifact...`;
}
} }
if (/\[AGENT:|\[PREVIEW:/.test(text)) { // Hide tags and partial tags from display
chatDisplay = text chatDisplay = text
.replace(/\[AGENT:[\w-]+\]/g, "") .replace(/\[AGENT:[\w-]+\]/g, "")
.replace(/\[PREVIEW:[\w-]+:?[\w-]+?\][\s\S]*?(?:\[\/PREVIEW\]|$)/g, "") .replace(/\[PREVIEW:[\w-]+:?[\w-]+?\][\s\S]*?(?:\[\/PREVIEW\]|$)/g, "")
.trim(); .replace(/\[(AGENT|PREVIEW)?(?::[\w-]*)?$/g, "") // Hide partial tags at the end
} .trim();
if (!preview) { if (!preview) {
const fenced = text.match(/```(html|css|javascript|tsx|jsx|md|markdown)\s*([\s\S]*?)```/i); const fenced = text.match(/```(html|css|javascript|tsx|jsx|md|markdown)\s*([\s\S]*?)```/i);
@@ -238,11 +233,11 @@ function parseStreamingContent(text: string) {
} }
} }
if (!chatDisplay && preview) { if (!chatDisplay && preview && preview.isStreaming) {
chatDisplay = `Rendering live artifact...`; chatDisplay = `Rendering live artifact...`;
} }
return { chatDisplay, preview, agent }; return { chatDisplay, preview, agent, status };
} }
// --- Main Component --- // --- Main Component ---
@@ -271,6 +266,8 @@ export default function AIAssist() {
const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle"); const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle");
const [aiPlan, setAiPlan] = useState<any>(null); const [aiPlan, setAiPlan] = useState<any>(null);
const [status, setStatus] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const isPreviewRenderable = (preview?: PreviewData | null) => { const isPreviewRenderable = (preview?: PreviewData | null) => {
if (!preview) return false; if (!preview) return false;
@@ -354,7 +351,9 @@ export default function AIAssist() {
currentAgent, currentAgent,
onChunk: (chunk) => { onChunk: (chunk) => {
accumulated += chunk; accumulated += chunk;
const { chatDisplay, preview, agent } = parseStreamingContent(accumulated); const { chatDisplay, preview, agent, status: streamStatus } = parseStreamingContent(accumulated);
if (streamStatus) setStatus(streamStatus);
// If we're in planning mode and see JSON, try to parse the plan // If we're in planning mode and see JSON, try to parse the plan
if (assistStep === "plan" || assistStep === "idle") { if (assistStep === "plan" || assistStep === "idle") {
@@ -415,6 +414,7 @@ export default function AIAssist() {
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
setAbortController(null); setAbortController(null);
setStatus(null);
} }
}; };
@@ -627,6 +627,19 @@ export default function AIAssist() {
</Button> </Button>
)} )}
</div> </div>
{msg.role === "assistant" && isProcessing && i === aiAssistHistory.length - 1 && status && (
<div className="flex items-center gap-3 px-4 py-2 bg-blue-500/5 dark:bg-blue-500/10 border border-blue-500/20 rounded-2xl animate-in slide-in-from-left-2 duration-300">
<div className="relative h-2 w-2">
<div className="absolute inset-0 bg-blue-500 rounded-full animate-ping" />
<div className="absolute inset-0 bg-blue-500 rounded-full" />
</div>
<span className="text-[10px] font-black text-blue-600 dark:text-blue-400 uppercase tracking-[0.15em]">
{status}
</span>
</div>
)}
<div className="flex items-center gap-2 px-2"> <div className="flex items-center gap-2 px-2">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter"> <span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">
{msg.role === "assistant" ? `Agent ${msg.agent || 'core'}` : 'Explorer'} {msg.role === "assistant" ? `Agent ${msg.agent || 'core'}` : 'Explorer'}
@@ -634,6 +647,12 @@ export default function AIAssist() {
</div> </div>
</div> </div>
))} ))}
{isProcessing && aiAssistHistory[aiAssistHistory.length - 1]?.role === "user" && (
<div className="flex flex-col items-start gap-3 animate-in fade-in duration-300">
<ThinkingIndicator />
</div>
)}
</div> </div>
{/* Input Area */} {/* Input Area */}