ChatPage.vue
1 <script setup lang="ts"> 2 import { computed, ref } from 'vue' 3 import { useDisplay } from 'vuetify' 4 import ImgDialog from '../common/ImgDialog.vue' 5 import ChatCard from '../common/ChatCard.vue' 6 import MarkdownCard from '../common/MarkdownCard.vue' 7 import SamplingCard from '../common/SamplingCard.vue' 8 import ElicitationCard from '../common/ElicitationCard.vue' 9 10 import { isEmptyTools } from '@/renderer/composables/chatCompletions' 11 12 interface Message { 13 role: 'user' | 'assistant' | 'tool' 14 content: any 15 tool_calls?: any[] 16 tool_call_id?: string 17 reasoning_content?: string 18 } 19 20 interface Group { 21 index: number 22 group: 'user' | 'assistant' | 'tool' 23 message?: Message 24 tab?: string 25 messages?: Message[] 26 length?: number 27 } 28 29 const { smAndUp } = useDisplay() 30 31 const props = defineProps<{ 32 messages: Message[] 33 }>() 34 35 const emit = defineEmits<{ 36 (_e: 'request-delete', _payload: { index: number; range: number }): void 37 }>() 38 39 const handleDeleteMessages = ({ index, range }: { index: number; range: number }) => { 40 emit('request-delete', { 41 index, 42 range 43 }) 44 } 45 46 const dialogs = ref<{ [key: string]: number }>({}) 47 const groupMessages = computed<Group[]>(() => { 48 const groups: Group[] = [] 49 props.messages.forEach((message, index) => { 50 if (message.role === 'user') { 51 groups.push({ 52 index, 53 group: 'user', 54 message 55 }) 56 } else if (message.role === 'assistant' && isEmptyTools(message.tool_calls)) { 57 groups.push({ 58 index, 59 group: 'assistant', 60 message 61 }) 62 } else { 63 const lastGroup = groups[groups.length - 1] 64 if (lastGroup?.group === 'tool') { 65 lastGroup.messages?.push(message) 66 dialogs.value[lastGroup.tab!] = lastGroup.length! 67 lastGroup.length! += 1 68 } else { 69 const id = message.tool_call_id || message.tool_calls?.[0]?.id 70 groups.push({ 71 index, 72 group: 'tool', 73 tab: id, 74 messages: [message], 75 length: 1 76 }) 77 dialogs.value[id!] = 0 78 } 79 } 80 }) 81 console.log(groups) 82 console.log(dialogs.value) 83 return groups 84 }) 85 </script> 86 87 <template> 88 <SamplingCard></SamplingCard> 89 <ElicitationCard></ElicitationCard> 90 <div v-for="group in groupMessages" :key="group.index"> 91 <!-- User Messages --> 92 <div v-if="group.group === 'user'"> 93 <div class="px-2 py-5 chat-message"> 94 <div class="message"> 95 <v-avatar 96 class="mt-2 mr-3 mr-lg-6" 97 :size="smAndUp ? 'default' : 'x-small'" 98 color="primary" 99 variant="tonal" 100 icon="mdi-account-circle" 101 /> 102 <chat-card 103 :index="group.index" 104 :messages="messages" 105 :show-modify="true" 106 @delete-messages="handleDeleteMessages" 107 > 108 <template #default="{ showmodify }"> 109 <v-card-text v-if="Array.isArray(group.message!.content)" class="md-preview pt-1"> 110 <div v-for="(item, index) in group.message!.content" :key="index"> 111 <img-dialog v-if="item.type === 'image_url'" :src="item.image_url.url" /> 112 <v-textarea 113 v-model="item.text" 114 class="conversation-area" 115 variant="plain" 116 density="compact" 117 auto-grow 118 :counter="showmodify || undefined" 119 :hide-details="!showmodify" 120 rows="1" 121 :readonly="!showmodify" 122 /> 123 </div> 124 </v-card-text> 125 <v-card-text v-else class="md-preview pt-1"> 126 <v-textarea 127 v-model="group.message!.content" 128 class="conversation-area" 129 variant="plain" 130 density="compact" 131 auto-grow 132 rows="1" 133 :readonly="!showmodify" 134 :counter="showmodify || undefined" 135 :hide-details="!showmodify" 136 /> 137 </v-card-text> 138 </template> 139 </chat-card> 140 </div> 141 </div> 142 </div> 143 144 <!-- Assistant Messages --> 145 <div v-if="group.group === 'assistant'"> 146 <div class="px-2 py-5 chat-message"> 147 <div class="message"> 148 <v-avatar 149 class="mt-2 mr-3 mr-lg-6" 150 :size="smAndUp ? 'default' : 'x-small'" 151 color="teal" 152 variant="tonal" 153 icon="mdi-lightning-bolt-circle" 154 /> 155 <chat-card 156 :index="group.index" 157 :messages="messages" 158 :show-content="true" 159 @delete-messages="handleDeleteMessages" 160 > 161 <template #default="{ showcontent }"> 162 <v-card-text v-if="group.message!.reasoning_content" class="md-preview pt-1"> 163 <v-textarea 164 v-model="group.message!.reasoning_content" 165 class="conversation-area text-disabled font-italic" 166 variant="plain" 167 density="compact" 168 auto-grow 169 hide-details 170 rows="1" 171 readonly 172 /> 173 </v-card-text> 174 <v-card-text v-if="showcontent" class="md-preview pt-1"> 175 <v-textarea 176 v-model="group.message!.content" 177 class="conversation-area" 178 variant="plain" 179 density="compact" 180 auto-grow 181 hide-details 182 rows="1" 183 readonly 184 /> 185 </v-card-text> 186 <v-card-text v-else class="md-preview py-2"> 187 <MarkdownCard :model-value="group.message!.content"></MarkdownCard> 188 </v-card-text> 189 </template> 190 </chat-card> 191 </div> 192 </div> 193 </div> 194 195 <!-- Tool Messages --> 196 <div v-if="group.group === 'tool'"> 197 <div class="px-2 py-5 chat-message"> 198 <div class="message"> 199 <v-avatar 200 class="mt-2 mr-3 mr-lg-6" 201 :size="smAndUp ? 'default' : 'x-small'" 202 color="brown" 203 variant="tonal" 204 icon="mdi-swap-vertical-circle" 205 /> 206 <chat-card 207 :messages="messages" 208 :show-copy="false" 209 :index="group.index" 210 :range="group.messages!.length" 211 @delete-messages="handleDeleteMessages" 212 > 213 <v-tabs v-model="dialogs[group.tab!]" show-arrows> 214 <v-tab 215 v-for="(item, index) in group.messages" 216 :key="index" 217 :text="item.role" 218 :value="index" 219 > 220 <v-icon 221 v-if="item.role === 'tool'" 222 icon="mdi-arrow-left-bold-circle" 223 color="primary" 224 /> 225 <v-icon 226 v-if="item.role === 'assistant'" 227 icon="mdi-arrow-right-bold-circle" 228 color="teal" 229 /> 230 </v-tab> 231 </v-tabs> 232 233 <v-tabs-window v-model="dialogs[group.tab!]"> 234 <v-tabs-window-item 235 v-for="(item, index) in group.messages" 236 :key="index" 237 :value="index" 238 > 239 <v-card v-if="item.role === 'tool'" class="mt-1" variant="flat"> 240 <v-card-item prepend-icon="mdi-chevron-left"> 241 <v-card-subtitle> 242 {{ item.tool_call_id }} 243 </v-card-subtitle> 244 </v-card-item> 245 <template v-if="Array.isArray(item.content)"> 246 <v-card-text v-for="content in item.content" :key="content.id"> 247 <v-textarea 248 :rows="1" 249 auto-grow 250 max-rows="15" 251 variant="plain" 252 :model-value="content.text" 253 hide-details 254 ></v-textarea> 255 </v-card-text> 256 </template> 257 <v-card-text v-else> 258 <v-textarea 259 :rows="1" 260 auto-grow 261 max-rows="15" 262 variant="plain" 263 :model-value="item.content" 264 hide-details 265 > 266 </v-textarea> 267 </v-card-text> 268 </v-card> 269 <v-card v-if="item.role === 'assistant'" class="mt-1" variant="flat"> 270 <v-card-text v-if="item.reasoning_content" class="font-weight-bold"> 271 {{ item.reasoning_content }} 272 </v-card-text> 273 <v-card-text v-if="item.content" class="font-weight-bold"> 274 {{ item.content }} 275 </v-card-text> 276 <div v-for="content in item.tool_calls" :key="content.id"> 277 <v-card-item prepend-icon="mdi-chevron-right"> 278 <v-card-subtitle> 279 {{ content.id }} 280 </v-card-subtitle> 281 </v-card-item> 282 <v-card-text> 283 {{ content.function.name }}({{ content.function.arguments }}) 284 </v-card-text> 285 </div> 286 </v-card> 287 </v-tabs-window-item> 288 </v-tabs-window> 289 </chat-card> 290 </div> 291 </div> 292 </div> 293 </div> 294 </template> 295 296 <style> 297 .chat-message { 298 border-bottom: 1px solid #e5e7eb; 299 } 300 301 .message { 302 margin: 0 auto; 303 display: flex; 304 } 305 306 .md-preview { 307 width: 100vw; 308 max-width: 100%; 309 } 310 311 .conversation-area { 312 margin: 0; 313 /* 6px 4px 4px 4px; */ 314 /* top left bot right*/ 315 } 316 </style>