wip: snapshot das alterações não commitadas (2026-10-20)

Origem: D:/Documents/Github/LocalAiApp
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0
This commit is contained in:
evandro committed 2026-10-20 00:48:09 -03:00
1 parent ac215b4229
commit e08189f640
17 files changed
+2914 -13236

No files matched your search

+90 -9
View File
@@ -3,9 +3,10 @@ import { ChatMessage } from '@/store/useAppStore';
import Markdown from '@ronradtke/react-native-markdown-display';
import * as Clipboard from 'expo-clipboard';
import { Bot, Check, Copy, User } from 'lucide-react-native';
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
import { MessageActions } from './MessageActions';
import { ThinkingBlock } from './ThinkingBlock';
interface MessageBubbleProps {
message: ChatMessage;
@@ -40,6 +41,37 @@ export function MessageBubble({
const showingStreaming = isStreaming && isLastAssistant;
const shouldShowActions = !isUser || showActions;
const [displayedContent, setDisplayedContent] = useState(message.content);
useEffect(() => {
if (!showingStreaming) {
setDisplayedContent(message.content);
return;
}
let animationFrameId: number;
let currentText = displayedContent;
const targetText = message.content;
if (targetText.length < currentText.length) {
setDisplayedContent(targetText);
return;
}
const animate = () => {
if (currentText.length < targetText.length) {
const diff = targetText.length - currentText.length;
const charsToAdd = Math.max(1, Math.ceil(diff / 4));
currentText = targetText.slice(0, currentText.length + charsToAdd);
setDisplayedContent(currentText);
animationFrameId = requestAnimationFrame(animate);
}
};
animationFrameId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationFrameId);
}, [message.content, showingStreaming]);
// UseMemo for dynamic colors
const markdownStyles = useMemo(() => ({
body: {
@@ -172,22 +204,56 @@ export function MessageBubble({
}), [copiedText]);
const renderContent = () => {
let activeContent = showingStreaming ? displayedContent : message.content;
if (showingStreaming && displayedContent === '' && message.content !== '') {
// Prevent empty bubble flicker on the first chunk
activeContent = message.content.slice(0, 1);
}
if (isUser) {
return (
<Text style={[styles.text, { color: colors.messageUserText }]}>
{message.content}
{activeContent}
</Text>
);
}
if (!message.content && showingStreaming) {
return null; // TypingIndicator handled externally
if (!activeContent && showingStreaming) {
return (
<View style={styles.dotsRow}>
<View style={[styles.dot, styles.dot1, { backgroundColor: colors.textMuted }]} />
<View style={[styles.dot, styles.dot2, { backgroundColor: colors.textMuted }]} />
<View style={[styles.dot, styles.dot3, { backgroundColor: colors.textMuted }]} />
</View>
);
}
const thinkMatch = activeContent.match(/<think>([\s\S]*?)(?:<\/think>|$)/);
let cleanContent = activeContent;
let thinkingContent = null;
let isThinkingComplete = false;
if (thinkMatch) {
thinkingContent = thinkMatch[1].trim();
isThinkingComplete = activeContent.includes('</think>');
cleanContent = activeContent.replace(thinkMatch[0], '').trim();
}
return (
<Markdown style={markdownStyles} rules={markdownRules}>
{message.content}
</Markdown>
<>
{thinkingContent !== null && (
<ThinkingBlock
content={thinkingContent}
isComplete={isThinkingComplete}
isStreaming={showingStreaming}
/>
)}
{cleanContent.length > 0 && (
<Markdown style={markdownStyles} rules={markdownRules}>
{cleanContent}
</Markdown>
)}
</>
);
};
@@ -234,10 +300,10 @@ export function MessageBubble({
</View>
{/* Action buttons */}
{(shouldShowActions || !isUser) && message.content.length > 0 && (
{(shouldShowActions || !isUser) && (showingStreaming ? displayedContent : message.content).length > 0 && (
<MessageActions
role={message.role}
content={message.content}
content={showingStreaming ? displayedContent : message.content}
onEdit={isUser ? onEdit : undefined}
onRegenerate={!isUser && isLastAssistant ? onRegenerate : undefined}
onDelete={onDelete}
@@ -357,4 +423,19 @@ const styles = StyleSheet.create({
fontSize: 13,
fontFamily: 'monospace',
},
dotsRow: {
flexDirection: 'row',
gap: 6,
alignItems: 'center',
minHeight: 22,
paddingHorizontal: 4,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
dot1: { opacity: 1.0 },
dot2: { opacity: 0.6 },
dot3: { opacity: 0.3 },
});