d3ro-voice/apps/mobile/app/(tabs)/talk.tsx
Yun Chan 660e622a93 refactor(ui-native): d3roNativePalette amber->main 개명 (WS-NATIVE)
- packages/ui-native theme.ts accent: amber->main, amberDim->dim, amberGlow->glow (값 보존)
- apps/mobile 20건 + apps/mobile-rn 18건 + ui-native 컴포넌트 9건 = 47 참조 치환
SKIP: Led/PhosphorText/Header color prop 'amber'(의미론적 컴포넌트 API, 별도 마이그레이션)
       green/greenGlow 키(amber계 범위外)
정책: docs/REFACTOR_POLICY.md DP1, 이식 인사이트 P7
2026-07-22 02:37:45 +09:00

224 lines
5.8 KiB
TypeScript

// apps/mobile/app/(tabs)/talk.tsx
// AI Chat tab — text+voice chat
// Design ref: docs/v3/designs/talk.html
import { useState, useRef } from 'react'
import {
View,
ScrollView,
TextInput,
StyleSheet,
Pressable,
KeyboardAvoidingView,
Platform
} from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import {
PhosphorText,
Led,
Header,
AppStatusBar,
d3roNativePalette,
d3roNativeFonts
} from '@d3ro/ui-native'
import { useI18n } from '@d3ro/i18n'
interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
}
export default function TalkScreen(): React.ReactElement {
const insets = useSafeAreaInsets()
const { t } = useI18n()
const scrollRef = useRef<ScrollView>(null)
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
role: 'assistant',
content: t('mobile.talk.greeting')
}
])
const [input, setInput] = useState('')
function handleSend(): void {
if (!input.trim()) return
const userMsg: ChatMessage = {
id: String(Date.now()),
role: 'user',
content: input.trim()
}
setMessages((prev) => [...prev, userMsg])
setInput('')
// Phase M-5: actual AI response
setTimeout(() => {
scrollRef.current?.scrollToEnd({ animated: true })
}, 100)
}
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={80}
>
<Header
title={t('mobile.talk.title')}
paddingTop={insets.top}
rightContent={
<View style={styles.headerRight}>
<PhosphorText variant="label" color="amber">{t('mobile.talk.live')}</PhosphorText>
<Led color="green" size={6} />
</View>
}
/>
{/* Chat Messages */}
<ScrollView
ref={scrollRef}
style={styles.chatArea}
contentContainerStyle={styles.chatContent}
>
{/* Date Badge */}
<View style={styles.dateBadge}>
<PhosphorText variant="label" color="muted">
{new Date().toLocaleDateString(undefined, {
month: 'short',
day: 'numeric'
}).toUpperCase()}
</PhosphorText>
</View>
{messages.map((msg) => (
<View
key={msg.id}
style={[
styles.bubble,
msg.role === 'user' ? styles.userBubble : styles.aiBubble
]}
>
<PhosphorText
variant="body"
color={msg.role === 'user' ? 'amber' : 'primary'}
style={styles.bubbleText}
>
{msg.content}
</PhosphorText>
<PhosphorText variant="label" color="muted" style={styles.bubbleLabel}>
{msg.role === 'user' ? t('mobile.talk.you') : t('mobile.talk.claude')}
</PhosphorText>
</View>
))}
</ScrollView>
{/* Input Area */}
<View style={styles.inputArea}>
<View style={styles.inputRow}>
<TextInput
style={styles.textInput}
placeholder={t('mobile.talk.placeholder')}
placeholderTextColor={d3roNativePalette.text.muted}
value={input}
onChangeText={setInput}
onSubmitEditing={handleSend}
returnKeyType="send"
/>
<Pressable style={styles.sendBtn} onPress={handleSend}>
<View style={styles.sendArrow} />
</Pressable>
</View>
<AppStatusBar style={styles.inputStatus} />
</View>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
chatArea: { flex: 1 },
chatContent: { padding: 20, paddingBottom: 20, gap: 16 },
dateBadge: {
alignSelf: 'center',
backgroundColor: d3roNativePalette.bg.card,
borderWidth: 1,
borderColor: d3roNativePalette.border.default,
borderRadius: 999,
paddingHorizontal: 12,
paddingVertical: 4,
marginBottom: 8
},
bubble: {
maxWidth: '85%',
padding: 16,
borderRadius: 16,
marginBottom: 20
},
aiBubble: {
alignSelf: 'flex-start',
backgroundColor: d3roNativePalette.bg.card,
borderWidth: 1,
borderColor: d3roNativePalette.border.default,
borderTopLeftRadius: 4
},
userBubble: {
alignSelf: 'flex-end',
backgroundColor: d3roNativePalette.accent.dim,
borderWidth: 1,
borderColor: 'rgba(255, 92, 53, 0.3)',
borderTopRightRadius: 4
},
bubbleText: { fontFamily: d3roNativeFonts.sans },
bubbleLabel: {
position: 'absolute',
bottom: -16,
fontSize: 9,
letterSpacing: 1
},
inputArea: {
backgroundColor: 'rgba(25, 25, 27, 0.95)',
borderTopWidth: 1,
borderTopColor: d3roNativePalette.border.default,
paddingHorizontal: 12,
paddingTop: 12,
paddingBottom: 8
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: d3roNativePalette.bg.inset,
borderRadius: 12,
borderWidth: 1,
borderColor: d3roNativePalette.border.default,
padding: 8
},
textInput: {
flex: 1,
color: d3roNativePalette.text.primary,
fontSize: 14,
paddingHorizontal: 8,
fontFamily: d3roNativeFonts.sans
},
sendBtn: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: d3roNativePalette.accent.dim,
borderWidth: 1,
borderColor: 'rgba(255, 92, 53, 0.3)',
justifyContent: 'center',
alignItems: 'center'
},
sendArrow: {
width: 0,
height: 0,
borderLeftWidth: 5,
borderRightWidth: 5,
borderBottomWidth: 8,
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: d3roNativePalette.accent.main
},
inputStatus: { paddingVertical: 4 }
})