DS 컴포넌트 9개 (ScreenPanel, WaveBars, AppStatusBar, Header, FilterChip 신규), 5개 탭 + 로그인 화면 D3RO 인스트루먼트 미학 적용, i18n 연결 (I18nProvider + 모바일 전용 키 80+개), 모노레포 workspaces에 apps/mobile 추가.
224 lines
5.8 KiB
TypeScript
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.amberDim,
|
|
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.amberDim,
|
|
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.amber
|
|
},
|
|
inputStatus: { paddingVertical: 4 }
|
|
})
|