Electron Forge【實戰】帶圖片的 AI 聊天

改用支持圖片的 AI 模型

qwen-turbo 僅支持文字,要想體驗圖片聊天,需改用 qwen-vl-plus

src/initData.ts

  {id: 2,name: "aliyun",title: "阿里 -- 通義千問",desc: "阿里百煉 -- 通義千問",// https://help.aliyun.com/zh/dashscope/developer-reference/api-details?spm=a2c4g.11186623.0.0.5bf41507xgULX5#b148acc634pfcmodels: ["qwen-turbo", "qwen-vl-plus"],avatar:"https://qph.cf2.poecdn.net/main-thumb-pb-4160791-200-qlqunomdvkyitpedtghnhsgjlutapgfl.jpeg",},

安裝依賴 mime-types

用于便捷獲取圖片的類型

npm i mime-types @types/mime-types --save-dev

提問框中選擇本地圖片

在這里插入圖片描述

src/components/MessageInput.vue

<template><divclass="message-input w-full shadow-sm border rounded-lg border-gray-300 py-1 px-2 focus-within:border-green-700"><div v-if="imagePreview" class="my-2 relative inline-block"><img:src="imagePreview"alt="Preview"class="h-24 w-24 object-cover rounded"/><Iconicon="lets-icons:dell-fill"width="24"@click="delImg"class="absolute top-[-10px] right-[-10px] p-1 rounded-full cursor-pointer"/></div><div class="flex items-center"><inputtype="file"accept="image/*"ref="fileInput"class="hidden"@change="handleImageUpload"/><Iconicon="radix-icons:image"width="24"height="24":class="['mr-2',disabled? 'text-gray-300 cursor-not-allowed': 'text-gray-400 cursor-pointer hover:text-gray-600',]"@click="triggerFileInput"/><inputclass="outline-none border-0 flex-1 bg-white focus:ring-0"type="text"ref="ref_input"v-model="model":disabled="disabled":placeholder="tip"@keydown.enter="onCreate"/><Buttonicon-name="radix-icons:paper-plane"@click="onCreate":disabled="disabled">發送</Button></div></div>
</template><script lang="ts" setup>
import { ref } from "vue";
import { Icon } from "@iconify/vue";import Button from "./Button.vue";const props = defineProps<{disabled?: boolean;
}>();
const emit = defineEmits<{create: [value: string, imagePath?: string];
}>();
const model = defineModel<string>();
const fileInput = ref<HTMLInputElement | null>(null);
const imagePreview = ref("");
const triggerFileInput = () => {if (!props.disabled) {fileInput.value?.click();}
};
const tip = ref("");
let selectedImage: File | null = null;
const handleImageUpload = (event: Event) => {const target = event.target as HTMLInputElement;if (target.files && target.files.length > 0) {selectedImage = target.files[0];const reader = new FileReader();reader.onload = (e) => {imagePreview.value = e.target?.result as string;};reader.readAsDataURL(selectedImage);}
};
const onCreate = async () => {if (model.value && model.value.trim() !== "") {if (selectedImage) {const filePath = window.electronAPI.getFilePath(selectedImage);emit("create", model.value, filePath);} else {emit("create", model.value);}selectedImage = null;imagePreview.value = "";} else {tip.value = "請輸入問題";}
};
const ref_input = ref<HTMLInputElement | null>(null);const delImg = () => {selectedImage = null;imagePreview.value = "";
};defineExpose({ref_input: ref_input,
});
</script><style scoped>
input::placeholder {color: red;
}
</style>

src/preload.ts

需借助 webUtils 從 File 對象中獲取文件路徑

import { ipcRenderer, contextBridge, webUtils } from "electron";
 getFilePath: (file: File) => webUtils.getPathForFile(file),

將選擇的圖片,轉存到應用的用戶目錄

圖片很占空間,轉為字符串直接存入數據庫壓力過大,合理的方案是存到應用本地

src/views/Home.vue

在創建會話時執行

const createConversation = async (question: string, imagePath?: string) => {const [AI_providerName, AI_modelName] = currentProvider.value.split("/");let copiedImagePath: string | undefined;if (imagePath) {try {copiedImagePath = await window.electronAPI.copyImageToUserDir(imagePath);} catch (error) {console.error("拷貝圖片失敗:", error);}}// 用 dayjs 得到格式化的當前時間字符串const currentTime = dayjs().format("YYYY-MM-DD HH:mm:ss");// pinia 中新建會話,得到新的會話idconst conversationId = await conversationStore.createConversation({title: question,AI_providerName,AI_modelName,createdAt: currentTime,updatedAt: currentTime,msgList: [{type: "question",content: question,// 如果有圖片路徑,則將其添加到消息中...(copiedImagePath && { imagePath: copiedImagePath }),createdAt: currentTime,updatedAt: currentTime,},{type: "answer",content: "",status: "loading",createdAt: currentTime,updatedAt: currentTime,},],});// 更新當前選中的會話conversationStore.selectedId = conversationId;// 右側界面--跳轉到會話頁面 -- 帶參數 init 為新創建的會話的第一條消息idrouter.push(`/conversation/${conversationId}?type=new`);
};

src/preload.ts

  // 拷貝圖片到本地用戶目錄copyImageToUserDir: (sourcePath: string) =>ipcRenderer.invoke("copy-image-to-user-dir", sourcePath),

src/ipc.ts

  // 拷貝圖片到本地用戶目錄ipcMain.handle("copy-image-to-user-dir",async (event, sourcePath: string) => {const userDataPath = app.getPath("userData");const imagesDir = path.join(userDataPath, "images");await fs.mkdir(imagesDir, { recursive: true });const fileName = path.basename(sourcePath);const destPath = path.join(imagesDir, fileName);await fs.copyFile(sourcePath, destPath);return destPath;});

將圖片信息傳給 AI

src/views/Conversation.vue

發起 AI 聊天傳圖片參數

// 訪問 AI 模型,獲取答案
const get_AI_answer = async (answerIndex: number) => {await window.electronAPI.startChat({messageId: answerIndex,providerName: convsersation.value!.AI_providerName,selectedModel: convsersation.value!.AI_modelName,// 發給AI模型的消息需移除最后一條加載狀態的消息,使最后一條消息為用戶的提問messages: convsersation.value!.msgList.map((message) => ({role: message.type === "question" ? "user" : "assistant",content: message.content,// 若有圖片信息,則將其添加到消息中...(message.imagePath && { imagePath: message.imagePath }),})).slice(0, -1),});
};

繼續向 AI 提問時圖片參數

const sendNewMessage = async (question: string, imagePath?: string) => {let copiedImagePath: string | undefined;if (imagePath) {try {copiedImagePath = await window.electronAPI.copyImageToUserDir(imagePath);} catch (error) {console.error("拷貝圖片失敗:", error);}}// 獲取格式化的當前時間let currentTime = dayjs().format("YYYY-MM-DD HH:mm:ss");// 向消息列表中追加新的問題convsersation.value!.msgList.push({type: "question",content: question,...(copiedImagePath && { imagePath: copiedImagePath }),createdAt: currentTime,updatedAt: currentTime,});// 向消息列表中追加 loading 狀態的回答let new_msgList_length = convsersation.value!.msgList.push({type: "answer",content: "",createdAt: currentTime,updatedAt: currentTime,status: "loading",});// 消息列表的最后一條消息為 loading 狀態的回答,其id為消息列表的長度 - 1let loading_msg_id = new_msgList_length - 1;// 訪問 AI 模型獲取答案,參數為 loading 狀態的消息的idget_AI_answer(loading_msg_id);// 清空問題輸入框inputValue.value = "";await messageScrollToBottom();// 發送問題后,問題輸入框自動聚焦if (dom_MessageInput.value) {dom_MessageInput.value.ref_input.focus();}
};

src/providers/OpenAIProvider.ts

將消息轉換為 AI 模型需要的格式后傳給 AI

import OpenAI from "openai";
import { convertMessages } from "../util";interface ChatMessageProps {role: string;content: string;imagePath?: string;
}interface UniversalChunkProps {is_end: boolean;result: string;
}export class OpenAIProvider {private client: OpenAI;constructor(apiKey: string, baseURL: string) {this.client = new OpenAI({apiKey,baseURL,});}async chat(messages: ChatMessageProps[], model: string) {// 將消息轉換為AI模型需要的格式const convertedMessages = await convertMessages(messages);const stream = await this.client.chat.completions.create({model,messages: convertedMessages as any,stream: true,});const self = this;return {async *[Symbol.asyncIterator]() {for await (const chunk of stream) {yield self.transformResponse(chunk);}},};}protected transformResponse(chunk: OpenAI.Chat.Completions.ChatCompletionChunk): UniversalChunkProps {const choice = chunk.choices[0];return {is_end: choice.finish_reason === "stop",result: choice.delta.content || "",};}
}

src/util.ts

函數封裝 – 將消息轉換為 AI 模型需要的格式

import fs from 'fs/promises'
import { lookup } from 'mime-types'
export async function convertMessages( messages:  { role: string; content: string, imagePath?: string}[]) {const convertedMessages = []for (const message of messages) {let convertedContent: string | any[]if (message.imagePath) {const imageBuffer = await fs.readFile(message.imagePath)const base64Image = imageBuffer.toString('base64')const mimeType = lookup(message.imagePath)convertedContent = [{type: "text",text: message.content || ""},{type: 'image_url',image_url: {url: `data:${mimeType};base64,${base64Image}`}}]} else {convertedContent = message.content}const { imagePath, ...messageWithoutImagePath } = messageconvertedMessages.push({...messageWithoutImagePath,content: convertedContent})}return convertedMessages
}

加載消息記錄中的圖片

渲染進程中,無法直接讀取本地圖片,需借助 protocol 實現

src/main.ts

import { app, BrowserWindow, protocol, net } from "electron";
import { pathToFileURL } from "node:url";
import path from "node:path";// windows 操作系統必要
protocol.registerSchemesAsPrivileged([{scheme: "safe-file",privileges: {standard: true,secure: true,supportFetchAPI: true,},},
]);

在 createWindow 方法內執行

  protocol.handle("safe-file", async (request) => {const userDataPath = app.getPath("userData");const imageDir = path.join(userDataPath, "images");// 去除協議頭 safe-file://,解碼 URL 中的路徑const filePath = path.join(decodeURIComponent(request.url.slice("safe-file:/".length)));const filename = path.basename(filePath);const fileAddr = path.join(imageDir, filename);// 轉換為 file:// URLconst newFilePath = pathToFileURL(fileAddr).toString();// 使用 net.fetch 加載本地文件return net.fetch(newFilePath);});

頁面中渲染圖片

在這里插入圖片描述

src/components/MessageList.vue

img 的 src 添加了 safe-file:// 協議

          <div v-if="message.type === 'question'"><div class="mb-3 flex justify-end"><imgv-if="message.imagePath":src="`safe-file://${message.imagePath}`"alt="提問的配圖"class="h-24 w-24 object-cover rounded"/></div><divclass="message-question bg-green-700 text-white p-2 rounded-md">{{ message.content }}</div></div>

最終效果

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/79010.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/79010.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/79010.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在 Elastic 中使用 JOIN 進行威脅狩獵!

作者&#xff1a;來自 Elastic Paul Ewing, Jonhnathan Ribeiro Elastic 的管道查詢語言 ES | QL 為查詢帶來了 join 功能。 威脅狩獵者歡呼吧&#xff01;你是否一直在尋找一種通過 Elastic 的速度和強大功能來連接數據的方法&#xff1f;好消息&#xff01;Elastic 現在可以通…

從實列中學習linux shell5: 利用shell 腳本 檢測硬盤空間容量,當使用量達到80%的時候 發送郵件

下面是用于檢測硬盤空間并在使用量達到80%時發送郵件的Shell腳本 第一步 編寫腳本 #!/bin/bash# 郵件配置 recipient"zhaoqingyou99qhzt.com" subject"磁盤空間警報" mail_cmd"/usr/bin/mail" # 根據實際郵件命令路徑修改# 檢查是否安裝郵件工…

Ethan獨立開發產品日報 | 2025-04-30

1. Daytona 安全且靈活的基礎設施&#xff0c;用于運行你的人工智能生成代碼。 Daytona Cloud重新定義了AI代理的基礎設施&#xff0c;具備低于90毫秒的啟動時間、原生性能和有狀態執行能力&#xff0c;這些是傳統云服務無法比擬的。您可以以前所未有的速度和靈活性來創建、管…

Unity SpriteMask(精靈遮罩)

&#x1f3c6; 個人愚見&#xff0c;沒事寫寫筆記 &#x1f3c6;《博客內容》&#xff1a;Unity3D開發內容 &#x1f3c6;&#x1f389;歡迎 &#x1f44d;點贊?評論?收藏 &#x1f50e;SpriteMask&#xff1a;精靈遮罩 &#x1f4a1;作用就是對精靈圖片產生遮罩&#xff0c…

OpenHarmony全局資源調度管控子系統之內存管理部件

OpenHarmony之內存管理部件 內存管理部件 簡介目錄框架 進程回收優先級列表 補充 回收策略/查殺策略 使用說明參數配置說明 availbufferSizeZswapdParamkillConfignandlife 相關倉 簡介 內存管理部件位于全局資源調度管控子系統中&#xff0c;基于應用的生命周期狀態&#…

姜老師的MBTI課程筆記小結(1)ENFJ人格

課程文稿&#xff1a; 好&#xff0c;今天我們的重點其實并不在ENTJ&#xff0c;而是在于如果一個人其他都很像&#xff0c;只是在思考和感受這兩端選擇的時候&#xff0c;他缺了思考而更尊重感受&#xff0c;它會是什么樣的一個人格特質呢&#xff1f;這就是ENFG在16人格的學派…

Node.js 應用場景

Node.js 應用場景 引言 Node.js 是一個基于 Chrome V8 JavaScript 引擎的開源、跨平臺 JavaScript 運行環境。它主要用于服務器端開發&#xff0c;通過非阻塞 I/O 模型實現了高并發處理能力。本文將詳細介紹 Node.js 的應用場景&#xff0c;幫助你了解其在實際項目中的應用。…

Qt/C++面試【速通筆記六】—Qt 中的線程同步

在多線程編程中&#xff0c;多個線程同時訪問共享資源時&#xff0c;可能會出現數據不一致或者錯誤的情況。這時&#xff0c;我們需要線程同步機制來保證程序的正確性。Qt 提供了多種線程同步方式&#xff0c;每種方式適用于不同的場景。 1. 互斥鎖&#xff08;QMutex&#xff…

JDK-17 保姆級安裝教程(附安裝包)

文章目錄 一、下載二、安裝三、驗證是否安裝成功1、看 java 和 javac 是否可用2、看 java 和 javac 的版本號是否無問題 一、下載 JDK-17_windows-x64_bin.exe 二、安裝 三、驗證是否安裝成功 java&#xff1a;執行工具 javac&#xff1a;編譯工具 1、看 java 和 javac 是否…

【LeetCode Hot100】回溯篇

前言 本文用于整理LeetCode Hot100中題目解答&#xff0c;因題目比較簡單且更多是為了面試快速寫出正確思路&#xff0c;只做簡單題意解讀和一句話題解方便記憶。但代碼會全部給出&#xff0c;方便大家整理代碼思路。 46. 全排列 一句話題意 給定一個無重復數字的序列&#xf…

pytest-前后置及fixture運用

1.pytest中的xunit風格前后置處理 pytest中用例的前后置可以直接使用類似于unittest中的前后置處理&#xff0c;但是pytest中的前后置處理方式更 加豐富&#xff0c;分為模塊級、類級、方法級、函數級等不同等級的前后置處理&#xff0c;具體見下面的代碼&#xff1a; test_…

使用scipy求解優化問題

一、求解二次規劃問題 min(X.T * P * X C.T * X) s.t. Xi > 0 ∑Xi 1 1.定義目標函數 def objective(x):return 0.5 * np.dot(x, np.dot(P, x)) np.dot(c, x)2. 定義等式約束 def equality_constraint(x):return np.sum(x) - 1 3.定義邊界約束&#xff1a;x # …

C++初階-STL簡介

目錄 1.什么是STL 2.STL的版本 3.STL的六大組件 4.STL的重要性 4.1在筆試中 4.2在面試中 4.3.在公司中 5.如何學習STL 6.總結和之后的規劃 1.什么是STL STL&#xff08;standard template library-標準模板庫&#xff09;&#xff1b;是C標準庫的重要組成部分&#xf…

kivy android打包buildozer.spec GUI配置

這個適合剛剛學習kivyd的道友使用&#xff0c;后面看情況更新 代碼 import tkinter as tk from tkinter import ttk, filedialog, messagebox, simpledialog import configparser import os import json # 新增導入class BuildozerConfigTool:def __init__(self, master):se…

MOOS-ivp使用(一)——水下機器人系統的入門與使用

MOOS-ivp使用&#xff08;一&#xff09;——水下機器人系統的入門與使用 MOOS-ivp&#xff08;Marine Operational Oceanographic System for Intelligent Vehicle Planning&#xff09;是專為水下機器人&#xff08;如AUV&#xff09;設計的開源框架。類似于ROS&#xff0c;…

電子病歷高質量語料庫構建方法與架構項目(智能質控體系建設篇)

引言 隨著人工智能技術的迅猛發展,醫療信息化建設正經歷著前所未有的變革。電子病歷作為醫療機構的核心數據資產,其質量直接關系到臨床決策的準確性和醫療安全。傳統的病歷質控工作主要依賴人工審核,存在效率低下、主觀性強、覆蓋面有限等問題。近年來,基于人工智能技術的…

react學習筆記4——React UI組件庫與redux

流行的開源React UI組件庫 material-ui(國外) 官網: http://www.material-ui.com/#/github: GitHub - mui/material-ui: Material UI: Comprehensive React component library that implements Googles Material Design. Free forever. ant-design(國內螞蟻金服) 官網: Ant…

GPU集群搭建

1. 硬件規劃與采購 GPU 服務器&#xff1a;挑選契合需求的 GPU 服務器&#xff0c;像 NVIDIA DGX 系列就不錯&#xff0c;它集成了多個高性能 GPU。網絡設備&#xff1a;高速網絡設備不可或缺&#xff0c;例如萬兆以太網交換機或者 InfiniBand 交換機&#xff0c;以此保證節點…

ZYNQ 純PL端邏輯資源程序固化流程

ZYNQ 純PL端邏輯資源程序固化 ZYNQ的程序固化流程比傳統的FPGA固化流程復雜很多&#xff0c;Vivado生成的bit文件無法直接固化在ZYNQ芯片中。因為ZYNQ 非易失性存儲器的引腳&#xff08;如 SD 卡、QSPI Flash&#xff09;是 ZYNQ PS 部分的專用引腳。這些非易失性存儲器由 PS …