ClaudeChat.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <template>
  2. <div class="claude-chat">
  3. <div class="chat-container">
  4. <div class="messages" ref="messagesContainer">
  5. <div
  6. v-for="(message, index) in messages"
  7. :key="index"
  8. :class="['message', message.role]"
  9. >
  10. <div class="message-content">
  11. <div class="message-role">{{ message.role === 'user' ? '사용자' : 'Claude' }}</div>
  12. <div class="message-text">{{ message.content }}</div>
  13. </div>
  14. </div>
  15. <div v-if="isLoading" class="message assistant loading">
  16. <div class="message-content">
  17. <div class="message-role">Claude</div>
  18. <div class="message-text">
  19. <div class="typing-indicator">
  20. <span></span>
  21. <span></span>
  22. <span></span>
  23. </div>
  24. </div>
  25. </div>
  26. </div>
  27. </div>
  28. <div class="input-container">
  29. <div class="input-wrapper">
  30. <textarea
  31. v-model="newMessage"
  32. @keydown.enter.prevent="handleEnter"
  33. placeholder="메시지를 입력하세요..."
  34. rows="1"
  35. ref="messageInput"
  36. ></textarea>
  37. <button
  38. @click="sendMessage"
  39. :disabled="!newMessage.trim() || isLoading"
  40. class="send-button"
  41. >
  42. <Icon name="mdi:send" />
  43. </button>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. </template>
  49. <script setup>
  50. import { ref, nextTick, onMounted } from 'vue'
  51. const { sendStreamMessage } = useClaude()
  52. const messages = ref([])
  53. const newMessage = ref('')
  54. const isLoading = ref(false)
  55. const messagesContainer = ref(null)
  56. const messageInput = ref(null)
  57. const scrollToBottom = () => {
  58. nextTick(() => {
  59. if (messagesContainer.value) {
  60. messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
  61. }
  62. })
  63. }
  64. const handleEnter = (event) => {
  65. if (!event.shiftKey) {
  66. sendMessage()
  67. } else {
  68. newMessage.value += '\n'
  69. }
  70. }
  71. const sendMessage = async () => {
  72. if (!newMessage.value.trim() || isLoading.value) return
  73. const userMessage = newMessage.value.trim()
  74. newMessage.value = ''
  75. messages.value.push({
  76. role: 'user',
  77. content: userMessage
  78. })
  79. scrollToBottom()
  80. isLoading.value = true
  81. const assistantMessage = {
  82. role: 'assistant',
  83. content: ''
  84. }
  85. messages.value.push(assistantMessage)
  86. scrollToBottom()
  87. try {
  88. const result = await sendStreamMessage(
  89. userMessage,
  90. messages.value.slice(0, -1),
  91. (chunk, fullResponse) => {
  92. assistantMessage.content = fullResponse
  93. scrollToBottom()
  94. }
  95. )
  96. if (!result.success) {
  97. assistantMessage.content = `오류가 발생했습니다: ${result.error}`
  98. }
  99. } catch (error) {
  100. assistantMessage.content = `오류가 발생했습니다: ${error.message}`
  101. } finally {
  102. isLoading.value = false
  103. scrollToBottom()
  104. }
  105. }
  106. onMounted(() => {
  107. if (messageInput.value) {
  108. messageInput.value.focus()
  109. }
  110. })
  111. </script>
  112. <style scoped>
  113. .claude-chat {
  114. height: 100vh;
  115. display: flex;
  116. flex-direction: column;
  117. }
  118. .chat-container {
  119. flex: 1;
  120. display: flex;
  121. flex-direction: column;
  122. max-width: 800px;
  123. margin: 0 auto;
  124. padding: 20px;
  125. height: 100%;
  126. }
  127. .messages {
  128. flex: 1;
  129. overflow-y: auto;
  130. padding: 20px 0;
  131. scroll-behavior: smooth;
  132. }
  133. .message {
  134. margin-bottom: 20px;
  135. display: flex;
  136. }
  137. .message.user {
  138. justify-content: flex-end;
  139. }
  140. .message.assistant {
  141. justify-content: flex-start;
  142. }
  143. .message-content {
  144. max-width: 70%;
  145. padding: 15px;
  146. border-radius: 15px;
  147. word-wrap: break-word;
  148. }
  149. .message.user .message-content {
  150. background-color: #007bff;
  151. color: white;
  152. border-bottom-right-radius: 5px;
  153. }
  154. .message.assistant .message-content {
  155. background-color: #f8f9fa;
  156. color: #333;
  157. border-bottom-left-radius: 5px;
  158. border: 1px solid #e9ecef;
  159. }
  160. .message-role {
  161. font-size: 12px;
  162. font-weight: 600;
  163. margin-bottom: 5px;
  164. opacity: 0.7;
  165. }
  166. .message-text {
  167. white-space: pre-wrap;
  168. line-height: 1.5;
  169. }
  170. .input-container {
  171. padding: 20px 0;
  172. border-top: 1px solid #e9ecef;
  173. }
  174. .input-wrapper {
  175. display: flex;
  176. gap: 10px;
  177. align-items: flex-end;
  178. }
  179. textarea {
  180. flex: 1;
  181. min-height: 40px;
  182. max-height: 120px;
  183. padding: 12px 15px;
  184. border: 1px solid #ddd;
  185. border-radius: 20px;
  186. resize: none;
  187. font-family: inherit;
  188. font-size: 14px;
  189. line-height: 1.4;
  190. outline: none;
  191. transition: border-color 0.2s;
  192. }
  193. textarea:focus {
  194. border-color: #007bff;
  195. }
  196. .send-button {
  197. width: 40px;
  198. height: 40px;
  199. border: none;
  200. border-radius: 50%;
  201. background-color: #007bff;
  202. color: white;
  203. cursor: pointer;
  204. display: flex;
  205. align-items: center;
  206. justify-content: center;
  207. transition: background-color 0.2s;
  208. }
  209. .send-button:hover:not(:disabled) {
  210. background-color: #0056b3;
  211. }
  212. .send-button:disabled {
  213. background-color: #ccc;
  214. cursor: not-allowed;
  215. }
  216. .typing-indicator {
  217. display: flex;
  218. gap: 4px;
  219. align-items: center;
  220. }
  221. .typing-indicator span {
  222. width: 8px;
  223. height: 8px;
  224. border-radius: 50%;
  225. background-color: #007bff;
  226. animation: typing 1.4s infinite ease-in-out;
  227. }
  228. .typing-indicator span:nth-child(1) {
  229. animation-delay: -0.32s;
  230. }
  231. .typing-indicator span:nth-child(2) {
  232. animation-delay: -0.16s;
  233. }
  234. @keyframes typing {
  235. 0%, 80%, 100% {
  236. transform: scale(0);
  237. opacity: 0.5;
  238. }
  239. 40% {
  240. transform: scale(1);
  241. opacity: 1;
  242. }
  243. }
  244. .loading .message-text {
  245. display: flex;
  246. align-items: center;
  247. min-height: 20px;
  248. }
  249. </style>