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>