live2d-widgets
Version:
Live2D widget for web pages
236 lines (217 loc) • 6.09 kB
HTML
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>看板娘聊天平台</title>
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/css/all.min.css">
<script src="../dist/live2d.min.js"></script>
<style>
html, body {
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
#waifu {
bottom: 0;
left: 0;
line-height: 0;
margin-bottom: -10px;
position: fixed;
transform: translateY(3px);
transition: transform .3s ease-in-out, bottom 3s ease-in-out;
z-index: 1;
}
#waifu:hover {
transform: translateY(0);
}
#waifu-tips {
animation: shake 50s ease-in-out 5s infinite;
background-color: rgba(236, 217, 188, .5);
border: 1px solid rgba(224, 186, 140, .62);
border-radius: 12px;
box-shadow: 0 3px 15px 2px rgba(191, 158, 118, .2);
font-size: 14px;
line-height: 24px;
margin: -100px 20px;
min-height: 70px;
overflow: hidden;
padding: 5px 10px;
position: absolute;
text-overflow: ellipsis;
transition: opacity 1s;
width: 250px;
word-break: break-all;
}
#waifu-tips.waifu-tips-active {
opacity: 1;
transition: opacity .2s;
}
#waifu-tips span {
color: #0099cc;
}
#live2d {
cursor: grab;
height: 300px;
position: relative;
width: 300px;
}
#live2d:active {
cursor: grabbing;
}
textarea {
background-color: transparent;
border: none;
width: 100%;
resize: none;
min-height: 100px;
}
textarea:focus {
outline: none;
}
</style>
</head>
<body>
<div id="waifu">
<div id="waifu-tips"><textarea id="text" disabled="true"></textarea></div>
<canvas id="live2d" width="800" height="800"></canvas>
</div>
<script type="module">
/*
* _(:з」∠)_
* Created by Shuqiao Zhang in 2025.
* https://zhangshuqiao.org
*/
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
import { Cubism2Model } from "../build/index.js";
class ChatManager {
constructor() {
this.apiUrl = null;
this.apiKey = null;
this.conversationHistory = [];
this.systemPrompt = '请扮演Potion Maker中的Pio,在接下来的对话中,使用萌萌哒的语气回答问题。';
}
async create() {
// 提示用户输入API URL和Key
this.apiUrl = prompt("请输入大模型API URL (例如: https://api.openai.com/v1/chat/completions):");
if (!this.apiUrl) {
return "呜呜~ 你没有输入API URL呢,Pio无法开始对话啦~";
}
this.apiKey = prompt("请输入API Key:");
if (!this.apiKey) {
return "呜呜~ 你没有输入API Key呢,Pio无法开始对话啦~";
}
// 初始化对话历史,添加系统提示词
this.conversationHistory = [
{
role: "system",
content: this.systemPrompt
}
];
return "嗨~ 我是Pio哦!有什么可以帮助你的吗?(按Enter发送消息)";
}
async message(content) {
if (!this.apiUrl || !this.apiKey) {
return "呜呜~ API配置不完整,请刷新页面重新配置~";
}
// 添加用户消息到历史
this.conversationHistory.push({
role: "user",
content: content
});
try {
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
messages: this.conversationHistory,
temperature: 0.7,
max_tokens: 1000
})
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const assistantMessage = data.choices[0].message.content;
// 添加助手回复到历史
this.conversationHistory.push({
role: "assistant",
content: assistantMessage
});
return assistantMessage;
} catch (error) {
console.error("API调用出错:", error);
return `呜呜~ 出错了呢: ${error.message}`;
}
}
}
window.addEventListener("load", async () => {
"use strict";
const apiPath = "https://live2d.fghrsh.net/api";
const text = document.getElementById("text");
const manager = new ChatManager();
let state = 0, loading = false,
modelId = localStorage.getItem("modelId"),
modelTexturesId = localStorage.getItem("modelTexturesId");
if (modelId === null) {
modelId = 1;
modelTexturesId = 53;
}
const model = new Cubism2Model();
await loadModel(modelId, modelTexturesId);
async function loadModel(modelId, modelTexturesId) {
localStorage.setItem("modelId", modelId);
if (modelTexturesId === undefined) modelTexturesId = 0;
localStorage.setItem("modelTexturesId", modelTexturesId);
const modelSettingPath = `${apiPath}/get/?id=${modelId}-${modelTexturesId}`;
const response = await fetch(modelSettingPath);
const modelSetting = await response.json();
if (!model.gl) {
await model.init('live2d', modelSettingPath, modelSetting);
} else {
await model.changeModelWithJSON(modelSettingPath, modelSetting);
}
console.log("live2d", `模型 ${modelId}-${modelTexturesId} 加载完成`);
setTimeout(() => {
state = 2;
}, 2000);
}
text.addEventListener("keydown", async (e) => {
if (e.code === "Enter") {
e.preventDefault();
let content = text.value;
if (content === "") return;
console.log(content);
text.disabled = true;
text.value = await manager.message(content);
text.disabled = false;
}
});
const message = await manager.create();
text.value = message;
text.disabled = false;
});
</script>
</body>
</html>