/ src / renderer / components / common / SamplingCard.vue
SamplingCard.vue
  1  <script setup lang="ts">
  2  import { ref, watch } from 'vue'
  3  import { SamplingTransfer } from '@/renderer/utils'
  4  import { useSnackbarStore } from '@/renderer/store/snackbar'
  5  import { useChatbotStore } from '@/renderer/store/chatbot'
  6  import { useHistoryStore } from '@/renderer/store/history'
  7  import { useMessageStore } from '@/renderer/store/message'
  8  import { createCompletion } from '@/renderer/composables/chatCompletions'
  9  import ConfigJsonCard from './ConfigJsonCard.vue'
 10  
 11  import type { ChatCompletionResponseMessage } from '@/renderer/types/message'
 12  
 13  import { IpcSamplingRequestCallback, SamplingRequestParams, SamplingResponse } from '@/types/ipc'
 14  
 15  const snackbarStore = useSnackbarStore()
 16  
 17  const allChatbotStore = useChatbotStore()
 18  
 19  const messageStore = useMessageStore()
 20  
 21  const chatbotStore = allChatbotStore.chatbots[allChatbotStore.selectedChatbotId]
 22  
 23  const historyStore = useHistoryStore()
 24  
 25  const samplingDialog = ref(false)
 26  
 27  const samplingParams = ref<SamplingRequestParams | {}>({})
 28  
 29  const samplingResults = ref<ChatCompletionResponseMessage[]>([])
 30  
 31  const samplingChannel = ref('')
 32  
 33  const jsonError = ref<string | null>(null)
 34  
 35  function handleError(errorMessage: string | null) {
 36    jsonError.value = errorMessage
 37  }
 38  
 39  const samplingId = ref('')
 40  
 41  const tryCompletions = () => {
 42    if (jsonError.value) {
 43      snackbarStore.showErrorMessage(jsonError.value)
 44    } else {
 45      if ('messages' in samplingParams.value) {
 46        // const { messages, ...restParams } = samplingParams.value
 47        // restParams.target = samplingResults.value
 48        samplingId.value = historyStore.getDate()
 49        createCompletion(
 50          {
 51            id: samplingId.value,
 52            messages: samplingResults.value
 53          },
 54          samplingParams.value
 55        )
 56      }
 57    }
 58  }
 59  
 60  const clearSampling = () => {
 61    samplingDialog.value = false
 62    samplingParams.value = {}
 63    samplingChannel.value = ''
 64    samplingResults.value.length = 0
 65    return
 66  }
 67  
 68  function responseSampling(response: SamplingResponse) {
 69    if (samplingChannel.value) {
 70      SamplingTransfer.response(samplingChannel.value, response)
 71    }
 72    clearSampling()
 73  }
 74  
 75  const finishSampling = (index: number) => {
 76    const bestResponse: ChatCompletionResponseMessage = samplingResults.value[index]
 77    const response: SamplingResponse = {
 78      model: chatbotStore.model,
 79      role: bestResponse?.role || 'assistant',
 80      content: {
 81        type: 'text',
 82        text:
 83          bestResponse?.content ||
 84          bestResponse?.reasoning_content ||
 85          `No response from model ${chatbotStore.model}`
 86      }
 87    }
 88    responseSampling(response)
 89    return
 90  }
 91  
 92  const rejectSampling = () => {
 93    const response: SamplingResponse = {
 94      model: 'N/A',
 95      role: 'assistant',
 96      stopReason: 'Reject by user',
 97      content: {
 98        type: 'text',
 99        text: 'The sampling request was rejected by the user for containing non-compliant content.'
100      }
101    }
102    responseSampling(response)
103    return
104  }
105  
106  const handleProgress: IpcSamplingRequestCallback = (_event, progress) => {
107    console.log('Sampling', progress)
108    samplingDialog.value = true
109    samplingParams.value = progress.request.params
110    samplingChannel.value = progress.responseChannel
111  }
112  
113  SamplingTransfer.request(handleProgress)
114  
115  type SamplingProgress = {
116    auto: boolean
117    percent: number
118  }
119  
120  const samplingProgress = ref<SamplingProgress>({
121    auto: true,
122    percent: 0
123  })
124  
125  function startProgress(triggerEvent: Function) {
126    const duration = 5000
127    const increment = 100 / (duration / 100)
128    let current = samplingProgress.value.percent
129  
130    const interval = setInterval(() => {
131      current += increment
132      if (current >= 100) {
133        current = 100
134        clearInterval(interval)
135        triggerEvent()
136      }
137      if (samplingProgress.value.auto) {
138        samplingProgress.value.percent = current
139      } else {
140        clearInterval(interval)
141      }
142    }, 100)
143  }
144  
145  function triggerChatCompletion() {
146    if (samplingProgress.value.auto) {
147      tryCompletions()
148    }
149  }
150  
151  function triggerSamplingReturn() {
152    if (samplingProgress.value.auto) finishSampling(samplingResults.value.length - 1)
153  }
154  
155  watch(
156    () => samplingDialog.value,
157    (newVal) => {
158      if (newVal) {
159        // Dialog opened, clean stage
160        samplingProgress.value.percent = 0
161        continueAutoSampling()
162      }
163    }
164  )
165  
166  function continueAutoSampling() {
167    samplingProgress.value.auto = true
168    startProgress(() => {
169      if (samplingResults.value.length > 0) {
170        startProgress(triggerSamplingReturn)
171      } else {
172        const timeout = setTimeout(() => {
173          // Set a timeout if no generation is initiated within a period
174          unwatch()
175        }, 30000) // 30 sec
176        const unwatch = watch(
177          () => samplingId.value in messageStore.generating,
178          (val) => {
179            if (val) {
180              // Generation has started, the timer is no longer needed. The watch will track the progress.
181              clearTimeout(timeout)
182            } else {
183              clearTimeout(timeout)
184              // If ChatCompletion generating finished, return result
185              unwatch()
186              samplingProgress.value.percent = 0
187              startProgress(triggerSamplingReturn)
188            }
189          }
190        )
191        triggerChatCompletion()
192      }
193    })
194  }
195  </script>
196  
197  <template>
198    <!-- For UI visualization without chat -->
199    <!-- <v-btn @click="samplingDialog = true" color="surface-variant" text="Open Dialog" variant="flat"></v-btn> -->
200    <v-dialog v-model="samplingDialog" persistent max-width="80vw" max-height="80vh" scrollable>
201      <v-card :title="$t('sampling.title')">
202        <v-divider></v-divider>
203        <v-card-text>
204          <ConfigJsonCard
205            v-model="samplingParams"
206            @on-error="handleError"
207            @focus="samplingProgress.auto = false"
208          >
209          </ConfigJsonCard>
210          <v-data-iterator :items="samplingResults" :items-per-page="-1">
211            <template #default="{ items }">
212              <template v-for="(item, index) in items" :key="index">
213                <v-card class="mx-4">
214                  <v-card-text>
215                    <v-textarea
216                      v-if="item.raw.reasoning_content"
217                      class="conversation-area text-disabled font-italic"
218                      variant="plain"
219                      :model-value="item.raw.reasoning_content.trim()"
220                      outlined
221                      readonly
222                      auto-grow
223                    ></v-textarea>
224                    <v-textarea
225                      v-if="item.raw.content"
226                      variant="plain"
227                      :model-value="item.raw.content.trim()"
228                      outlined
229                      readonly
230                      auto-grow
231                    ></v-textarea>
232                  </v-card-text>
233                  <v-divider></v-divider>
234                  <v-card-actions>
235                    <v-spacer></v-spacer>
236                    <v-icon-btn
237                      v-tooltip:start="$t('sampling.confirm')"
238                      icon="mdi-check-bold"
239                      color="success"
240                      variant="plain"
241                      rounded="lg"
242                      @click="finishSampling(index)"
243                    ></v-icon-btn>
244                  </v-card-actions>
245                </v-card>
246                <br />
247              </template>
248            </template>
249          </v-data-iterator>
250        </v-card-text>
251        <v-divider></v-divider>
252        <v-card-actions>
253          <v-progress-linear
254            v-model="samplingProgress.percent"
255            class="ml-8 mr-4"
256            color="primary"
257            :indeterminate="samplingId in messageStore.generating"
258            rounded
259            @click="samplingProgress.auto = false"
260          ></v-progress-linear>
261          <v-spacer></v-spacer>
262          <v-btn
263            v-if="samplingProgress.auto"
264            v-tooltip:top="$t('sampling.pause')"
265            icon="mdi-pause"
266            color="primary"
267            variant="plain"
268            rounded="lg"
269            @click="samplingProgress.auto = false"
270          ></v-btn>
271          <v-btn
272            v-else
273            v-tooltip:top="$t('sampling.continue')"
274            icon="mdi-play"
275            color="primary"
276            variant="plain"
277            rounded="lg"
278            @click="continueAutoSampling()"
279          ></v-btn>
280          <v-btn
281            v-tooltip:top="$t('sampling.reject')"
282            icon="mdi-cancel"
283            color="error"
284            variant="plain"
285            rounded="lg"
286            @click="rejectSampling"
287          ></v-btn>
288          <v-btn
289            v-tooltip:top="$t('sampling.comp')"
290            :icon="samplingResults.length === 0 ? 'mdi-arrow-up' : 'mdi-autorenew'"
291            color="primary"
292            variant="plain"
293            rounded="lg"
294            @click="tryCompletions"
295          ></v-btn>
296          <v-btn
297            v-if="samplingResults.length > 0"
298            v-tooltip:top="$t('sampling.confirm-last')"
299            icon="mdi-check-bold"
300            color="success"
301            variant="plain"
302            rounded="lg"
303            @click="finishSampling(samplingResults.length - 1)"
304          ></v-btn>
305        </v-card-actions>
306      </v-card>
307    </v-dialog>
308  </template>