|
|
@@ -0,0 +1,288 @@
|
|
|
+<template>
|
|
|
+ <div class="claude-chat">
|
|
|
+ <div class="chat-container">
|
|
|
+ <div class="messages" ref="messagesContainer">
|
|
|
+ <div
|
|
|
+ v-for="(message, index) in messages"
|
|
|
+ :key="index"
|
|
|
+ :class="['message', message.role]"
|
|
|
+ >
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-role">{{ message.role === 'user' ? '사용자' : 'Claude' }}</div>
|
|
|
+ <div class="message-text">{{ message.content }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="isLoading" class="message assistant loading">
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-role">Claude</div>
|
|
|
+ <div class="message-text">
|
|
|
+ <div class="typing-indicator">
|
|
|
+ <span></span>
|
|
|
+ <span></span>
|
|
|
+ <span></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="input-container">
|
|
|
+ <div class="input-wrapper">
|
|
|
+ <textarea
|
|
|
+ v-model="newMessage"
|
|
|
+ @keydown.enter.prevent="handleEnter"
|
|
|
+ placeholder="메시지를 입력하세요..."
|
|
|
+ rows="1"
|
|
|
+ ref="messageInput"
|
|
|
+ ></textarea>
|
|
|
+ <button
|
|
|
+ @click="sendMessage"
|
|
|
+ :disabled="!newMessage.trim() || isLoading"
|
|
|
+ class="send-button"
|
|
|
+ >
|
|
|
+ <Icon name="mdi:send" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, nextTick, onMounted } from 'vue'
|
|
|
+
|
|
|
+const { sendStreamMessage } = useClaude()
|
|
|
+
|
|
|
+const messages = ref([])
|
|
|
+const newMessage = ref('')
|
|
|
+const isLoading = ref(false)
|
|
|
+const messagesContainer = ref(null)
|
|
|
+const messageInput = ref(null)
|
|
|
+
|
|
|
+const scrollToBottom = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ if (messagesContainer.value) {
|
|
|
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleEnter = (event) => {
|
|
|
+ if (!event.shiftKey) {
|
|
|
+ sendMessage()
|
|
|
+ } else {
|
|
|
+ newMessage.value += '\n'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const sendMessage = async () => {
|
|
|
+ if (!newMessage.value.trim() || isLoading.value) return
|
|
|
+
|
|
|
+ const userMessage = newMessage.value.trim()
|
|
|
+ newMessage.value = ''
|
|
|
+
|
|
|
+ messages.value.push({
|
|
|
+ role: 'user',
|
|
|
+ content: userMessage
|
|
|
+ })
|
|
|
+
|
|
|
+ scrollToBottom()
|
|
|
+ isLoading.value = true
|
|
|
+
|
|
|
+ const assistantMessage = {
|
|
|
+ role: 'assistant',
|
|
|
+ content: ''
|
|
|
+ }
|
|
|
+
|
|
|
+ messages.value.push(assistantMessage)
|
|
|
+ scrollToBottom()
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await sendStreamMessage(
|
|
|
+ userMessage,
|
|
|
+ messages.value.slice(0, -1),
|
|
|
+ (chunk, fullResponse) => {
|
|
|
+ assistantMessage.content = fullResponse
|
|
|
+ scrollToBottom()
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!result.success) {
|
|
|
+ assistantMessage.content = `오류가 발생했습니다: ${result.error}`
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ assistantMessage.content = `오류가 발생했습니다: ${error.message}`
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ scrollToBottom()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ if (messageInput.value) {
|
|
|
+ messageInput.value.focus()
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.claude-chat {
|
|
|
+ height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.chat-container {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ max-width: 800px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 20px;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.messages {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 20px 0;
|
|
|
+ scroll-behavior: smooth;
|
|
|
+}
|
|
|
+
|
|
|
+.message {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.message.user {
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.message.assistant {
|
|
|
+ justify-content: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.message-content {
|
|
|
+ max-width: 70%;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 15px;
|
|
|
+ word-wrap: break-word;
|
|
|
+}
|
|
|
+
|
|
|
+.message.user .message-content {
|
|
|
+ background-color: #007bff;
|
|
|
+ color: white;
|
|
|
+ border-bottom-right-radius: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.message.assistant .message-content {
|
|
|
+ background-color: #f8f9fa;
|
|
|
+ color: #333;
|
|
|
+ border-bottom-left-radius: 5px;
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+.message-role {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+
|
|
|
+.message-text {
|
|
|
+ white-space: pre-wrap;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.input-container {
|
|
|
+ padding: 20px 0;
|
|
|
+ border-top: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+.input-wrapper {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+textarea {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 40px;
|
|
|
+ max-height: 120px;
|
|
|
+ padding: 12px 15px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 20px;
|
|
|
+ resize: none;
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.4;
|
|
|
+ outline: none;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+textarea:focus {
|
|
|
+ border-color: #007bff;
|
|
|
+}
|
|
|
+
|
|
|
+.send-button {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #007bff;
|
|
|
+ color: white;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.send-button:hover:not(:disabled) {
|
|
|
+ background-color: #0056b3;
|
|
|
+}
|
|
|
+
|
|
|
+.send-button:disabled {
|
|
|
+ background-color: #ccc;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.typing-indicator {
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.typing-indicator span {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #007bff;
|
|
|
+ animation: typing 1.4s infinite ease-in-out;
|
|
|
+}
|
|
|
+
|
|
|
+.typing-indicator span:nth-child(1) {
|
|
|
+ animation-delay: -0.32s;
|
|
|
+}
|
|
|
+
|
|
|
+.typing-indicator span:nth-child(2) {
|
|
|
+ animation-delay: -0.16s;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes typing {
|
|
|
+ 0%, 80%, 100% {
|
|
|
+ transform: scale(0);
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
+ 40% {
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.loading .message-text {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 20px;
|
|
|
+}
|
|
|
+</style>
|