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:
1 parent
ac215b4229
commit
e08189f640
17 files changed
+2914
-13236
No files matched your search
@@ -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 },
|
||||
});
|
||||
Reference in new issue
Block a user