talktalkai-tool / live2dcubismcore.js
kevinwang676's picture
Create live2dcubismcore.js
a8352ae verified
<!DOCTYPE html>
<html lang="zh">
<head>
<!-- Google Analytics tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Y2Z5KKXXL7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-Y2Z5KKXXL7');
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><g transform='translate(256,256) scale(1.1) translate(-256,-256)'><path fill='%2387CEEB' d='M411.1,217.9c-9.7-43.5-48.4-76.2-94.6-76.2c-28.6,0-54.3,12.9-71.9,33.2c-9.2-3.2-19.1-5-29.4-5 c-32.1,0-58.4,24.7-62.3,56.5c-0.6,0-1.2,0-1.8,0c-38.6,0-70.1,31.6-70.1,70.1c0,38.5,31.5,70,70.1,70h229.9 c38.5,0,70.1-31.5,70.1-70C480,248.9,449.8,217.9,411.1,217.9z'/></g></svg>">
<title>书梦 - 书写你的每一个梦</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/animate.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&display=swap" rel="stylesheet">
<link href="/static/live2d/js/live2d.css" rel="stylesheet">
<style>
:root {
--primary-color: #FF6B6B;
--secondary-color: #4ECDC4;
--accent-color: #FFE66D;
--text-color: #2C3E50;
--bg-color: #F7F9FC;
--chat-bg: #FFFFFF;
--message-user: #E3F2FD;
--message-ai: #F5F5F5;
}
body {
font-family: 'Noto Serif SC', "PingFang SC", "Microsoft YaHei", serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.8;
}
.navbar {
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
padding-top: 0.3rem;
padding-bottom: 0.3rem;
}
.navbar-brand {
font-size: 2rem;
font-weight: bold;
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
padding: 0.3rem 0;
}
.brand-subtitle {
font-size: 1.05rem;
color: #FF6B6B;
font-weight: bold;
opacity: 0.8;
margin: 0;
padding: 0.3rem 0;
position: absolute;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
}
.brand-subtitle2 {
font-size: 1rem;
color: #FF6B6B;
}
.hero-section {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
position: relative;
overflow: hidden;
padding: 8rem 0;
margin-bottom: -6rem;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><circle cx="2" cy="2" r="1" fill="rgba(255,255,255,0.1)"/></svg>') repeat;
opacity: 0.3;
animation: float 20s linear infinite;
}
@keyframes float {
from { transform: translate(0, 0); }
to { transform: translate(20px, 20px); }
}
.chat-container {
background: var(--chat-bg);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-top: -2rem;
position: relative;
z-index: 1;
min-height: 600px;
}
.chat-messages {
height: 400px;
overflow-y: auto;
padding: 1.5rem;
}
.message {
margin-bottom: 1.5rem;
max-width: 80%;
position: relative;
}
.message-user {
margin-left: auto;
background: var(--message-user);
border-radius: 20px 20px 5px 20px;
padding: 1rem 1.5rem;
}
.message-ai {
margin-right: auto;
background: var(--message-ai);
border-radius: 20px 20px 20px 5px;
padding: 1rem 1.5rem;
}
.chat-input {
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding: 1.5rem;
background: white;
border-radius: 0 0 20px 20px;
}
.btn-primary {
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
border: none;
border-radius: 25px;
padding: 0.8rem 2rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.form-control {
border-radius: 15px;
padding: 0.8rem 1.2rem;
border: 2px solid #eee;
transition: all 0.3s ease;
}
.form-control:focus {
border-color: var(--secondary-color);
box-shadow: 0 0 0 0.2rem rgba(78, 205, 196, 0.25);
}
.feature-card {
background: white;
border-radius: 20px;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
border: none;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
}
.feature-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.12);
}
.feature-icon {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
color: white;
font-size: 2rem;
}
.footer {
background: var(--text-color);
color: white;
padding: 4rem 0 2rem;
margin-top: 4rem;
}
.social-links a {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
color: white;
margin: 0 0.5rem;
transition: all 0.3s ease;
}
.social-links a:hover {
background: var(--secondary-color);
transform: translateY(-3px);
}
.nav-link {
color: var(--text-color) !important;
font-weight: 500;
padding: 0.5rem 1.2rem;
border-radius: 20px;
transition: all 0.3s ease;
}
.nav-link:hover {
background: rgba(255, 107, 107, 0.1);
color: var(--primary-color) !important;
}
.loading-dots:after {
content: '.';
animation: dots 1.5s steps(5, end) infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60% { content: '...'; }
80% { content: '....'; }
100% { content: '.....'; }
}
.fancy-poetry2 {
font-family: "STKaiti", "KaiTi", serif;
background: linear-gradient(120deg, #4568dc, #b06ab3);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 1.25rem;
letter-spacing: 1px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
display: inline-block;
vertical-align: middle;
line-height: normal;
margin-bottom: 7px;
}
.navbar-toggler {
border: none;
padding: 0.5rem;
color: var(--text-color);
background: transparent;
}
.navbar-toggler:focus {
box-shadow: none;
outline: none;
}
.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(44, 62, 80, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
width: 24px;
height: 24px;
}
@media (max-width: 991.98px) {
.navbar-nav {
background: rgba(255, 255, 255, 0.98);
padding: 1rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-top: 1rem;
}
.nav-item {
margin: 0.5rem 0;
}
.brand-subtitle {
display: none;
}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-feather-alt me-2"></i>书梦
</a>
<div class="brand-subtitle">创造你的热爱</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-home me-1"></i>首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('text_to_speech_page') }}"><i class="fas fa-microphone me-1"></i><span class="fancy-poetry2">梦语</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('forum') }}"><i class="fas fa-comments me-1"></i><span class="fancy-poetry2">书梦社</span></a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item dropdown position-relative">
{% if request.endpoint != 'index' %}
<a class="nav-link" href="#" id="notificationsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-bell"></i>
<span id="notificationCount" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="display: none;">
0
</span>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="notificationsDropdown" style="width: 300px; max-height: 400px; overflow-y: auto;">
<div id="notificationList" class="px-2">
<!-- Notifications will be loaded here -->
</div>
</div>
{% endif %}
</li>
<li class="nav-item">
<a class="nav-link" href="/my-stories"><i class="fas fa-book me-1"></i>我的梦</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings"><i class="fas fa-cog me-1"></i>设置</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="/login"><i class="fas fa-sign-in-alt me-1"></i>登录</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% block content %}{% endblock %}
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-md-4 mb-4">
<h4><i class="fas fa-feather-alt me-2"></i>书梦</h4>
<p class="mb-0 brand-subtitle2" style="margin-top: 10px; letter-spacing: 0.2em;">创造你的热爱</p>
<p class="mb-0" style="margin-top: 10px;">宇宙这么大,总有一个属于你的梦</p>
</div>
<div class="col-md-4 mb-4">
<h5>快速链接</h5>
<ul class="list-unstyled">
<li><a href="/" class="text-white text-decoration-none">首页</a></li>
<li><a href="/login" class="text-white text-decoration-none">登录</a></li>
<li><a href="https://www.bilibili.com/video/BV1JBPkeQEt5" target="_blank" class="text-white text-decoration-none">使用指南</a></li>
</ul>
</div>
<div class="col-md-4 mb-4">
<h5>关注我们</h5>
<div class="social-links mt-3">
<a href="#" data-bs-toggle="modal" data-bs-target="#weixinQRModal"><i class="fab fa-weixin"></i></a>
<a href="https://space.bilibili.com/3546380060592726"><i class="fab fa-bilibili"></i></a>
<p class="mb-0" style="margin-top: 10px;">官方邮箱:[email protected]</p>
</div>
</div>
</div>
<div class="text-center mt-4">
<p class="mb-0">© 2025 书梦 DoingDream. All rights reserved.</p>
</div>
</div>
</footer>
<!-- Add the QR code modal -->
<div class="modal fade" id="weixinQRModal" tabindex="-1" aria-labelledby="weixinQRModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="weixinQRModalLabel">微信扫码关注我们吧~</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img src="{{ url_for('static', filename='qrcode.jpg') }}" alt="WeChat QR Code" class="img-fluid">
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/live2d/core/live2dcubismcore.min.js"></script>
<script src="/static/live2d/js/live2d.js"></script>
{% block extra_js %}
<script>
// Add notification handling functions
function updateNotificationCount() {
fetch('/notifications/count')
.then(response => response.json())
.then(data => {
const countBadge = document.getElementById('notificationCount');
if (data.count > 0) {
countBadge.textContent = data.count;
countBadge.style.display = 'inline-block';
} else {
countBadge.style.display = 'none';
}
});
}
// Function to get post_id for a comment
async function getPostIdForComment(comment_id) {
try {
const response = await fetch(`/get_post_id_for_comment/${comment_id}`);
const data = await response.json();
return data.post_id;
} catch (error) {
console.error('Error getting post_id for comment:', error);
return null;
}
}
async function loadNotifications() {
try {
const response = await fetch('/notifications');
const data = await response.json();
const notificationList = document.getElementById('notificationList');
notificationList.innerHTML = '';
if (data.notifications.length === 0) {
notificationList.innerHTML = '<div class="text-center py-3">暂无新消息</div>';
return;
}
// Create a map to store post IDs for comments to avoid multiple requests
const commentPostIds = new Map();
// First, gather all comment IDs that need post IDs
const commentPromises = data.notifications
.filter(n => n.comment_id && !n.post_id)
.map(async n => {
if (!commentPostIds.has(n.comment_id)) {
try {
const response = await fetch(`/get_post_id_for_comment/${n.comment_id}`);
const data = await response.json();
commentPostIds.set(n.comment_id, data.post_id);
} catch (error) {
console.error('Error fetching post ID:', error);
commentPostIds.set(n.comment_id, null);
}
}
});
// Wait for all post ID requests to complete
await Promise.all(commentPromises);
// Now render all notifications
for (const notification of data.notifications) {
let message = '';
let link = '#';
switch (notification.type) {
case 'follow':
message = `${notification.sender.username} 关注了你`;
link = `/user/${notification.sender.id}/profile`;
break;
case 'like':
if (notification.comment_id) {
message = `${notification.sender.username} 点赞了你的评论`;
const post_id = notification.post_id || commentPostIds.get(notification.comment_id);
link = `/forum/post/${post_id}#comment-${notification.comment_id}`;
} else {
message = `${notification.sender.username} 点赞了你的分享`;
link = `/forum/post/${notification.post_id}`;
}
break;
case 'comment':
if (notification.comment_id) {
message = `${notification.sender.username} 回复了你的评论`;
const post_id = notification.post_id || commentPostIds.get(notification.comment_id);
link = `/forum/post/${post_id}#comment-${notification.comment_id}`;
} else {
message = `${notification.sender.username} 评论了你的分享`;
link = `/forum/post/${notification.post_id}`;
}
break;
}
const notificationHtml = `
<a href="${link}" class="dropdown-item py-2 border-bottom" onclick="handleNotificationClick(event, '${link}')">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<img src="${notification.sender.avatar_url ? notification.sender.avatar_url : notification.sender.get_avatar()}"
class="rounded-circle"
width="40" height="40"
alt="${notification.sender.username}"
style="object-fit: cover; border: 1px solid rgba(0, 0, 0, 0.1);"
onerror="this.onerror=null; this.src='/static/default_avatar.png';">
</div>
<div class="flex-grow-1 ms-3">
<div class="text-wrap mb-1">${message}</div>
<small class="text-muted">${new Date(new Date(notification.created_at).getTime() + 8 * 60 * 60 * 1000).toLocaleString()}</small>
</div>
</div>
</a>
`;
notificationList.insertAdjacentHTML('beforeend', notificationHtml);
}
} catch (error) {
console.error('Error loading notifications:', error);
const notificationList = document.getElementById('notificationList');
notificationList.innerHTML = '<div class="text-center py-3 text-danger">加载消息失败</div>';
}
}
// Function to handle notification clicks
function handleNotificationClick(event, link) {
event.preventDefault();
// Close the dropdown
const dropdownMenu = document.querySelector('.dropdown-menu');
if (dropdownMenu) {
const dropdown = bootstrap.Dropdown.getInstance(document.getElementById('notificationsDropdown'));
if (dropdown) {
dropdown.hide();
}
}
// Navigate to the link
window.location.href = link;
}
// Add styles for notification items
const style = document.createElement('style');
style.textContent = `
.dropdown-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
#notificationList .dropdown-item {
white-space: normal;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
#notificationList .dropdown-item:last-child {
border-bottom: none;
}
#notificationList .dropdown-menu {
max-height: 400px;
overflow-y: auto;
}
`;
document.head.appendChild(style);
// Update notification count every minute
setInterval(updateNotificationCount, 60000);
// Load notifications when dropdown is opened
document.getElementById('notificationsDropdown')?.addEventListener('show.bs.dropdown', loadNotifications);
// Initial load
if (document.getElementById('notificationCount')) {
updateNotificationCount();
}
</script>
{% endblock %}
</body>
</html>