/ src / renderer / components / pages / ChatPage.vue
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>