first commit
This commit is contained in:
commit
97e6eddfb9
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
docker-compose.yml
|
203
.gitignore
vendored
Normal file
203
.gitignore
vendored
Normal file
@ -0,0 +1,203 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/macos,node,windows
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,node,windows
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,node,windows
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@ -0,0 +1 @@
|
||||
nodejs 20.17.0
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM node:20.17.0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
RUN npm rebuild
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "index.js"]
|
61
compose.yml
Normal file
61
compose.yml
Normal file
@ -0,0 +1,61 @@
|
||||
services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["redis-server", "--requirepass", "$REDIS_PASSWORD"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: mysql
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: $DB_PASSWORD
|
||||
MYSQL_DATABASE: $DB_NAME
|
||||
MYSQL_USER: $DB_USER
|
||||
MYSQL_PASSWORD: $DB_PASSWORD
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
- ./my.cnf:/etc/mysql/conf.d/my.cnf
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
web:
|
||||
container_name: imaginar
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
DB_HOST: $DB_HOST
|
||||
DB_PORT: $DB_PORT
|
||||
DB_USER: $DB_USER
|
||||
DB_PASSWORD: $DB_PASSWORD
|
||||
DB_NAME: $DB_NAME
|
||||
command: npm run dev
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
db_data:
|
32
controllers/accountController.js
Normal file
32
controllers/accountController.js
Normal file
@ -0,0 +1,32 @@
|
||||
const { User } = require('../models');
|
||||
|
||||
// アカウントセットアップページを表示
|
||||
exports.showAccountSetupPage = (req, res) => {
|
||||
res.render('accountSetup', { user: req.user, hideSidebar: true });
|
||||
};
|
||||
|
||||
// アカウント作成処理
|
||||
exports.handleAccountSetup = async (req, res) => {
|
||||
try {
|
||||
const { name, email, agreeToTerms } = req.body;
|
||||
|
||||
if (!agreeToTerms) {
|
||||
return res.status(400).send('利用規約に同意する必要があります');
|
||||
}
|
||||
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
// ユーザー情報を更新
|
||||
user.name = name;
|
||||
user.email = email;
|
||||
user.isAccountSetupComplete = true;
|
||||
|
||||
await user.save();
|
||||
|
||||
// アカウント登録が完了したら同位体作成画面へリダイレクト
|
||||
res.redirect('/isotope');
|
||||
} catch (error) {
|
||||
console.error('アカウント登録に失敗しました:', error);
|
||||
res.status(500).send('アカウント登録に失敗しました');
|
||||
}
|
||||
};
|
31
controllers/authController.js
Normal file
31
controllers/authController.js
Normal file
@ -0,0 +1,31 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
exports.handleCallback = async (req, res) => {
|
||||
try {
|
||||
if (!req.user.isAccountSetupComplete) {
|
||||
res.redirect('/register');
|
||||
} else {
|
||||
const sessionExpiration = parseInt(process.env.SESSION_EXPIRATION, 10);
|
||||
const token = jwt.sign(
|
||||
{ userId: req.user.id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: sessionExpiration }
|
||||
);
|
||||
|
||||
// JWTトークンをクッキーに保存
|
||||
res.cookie('jwt', token, { httpOnly: true, maxAge: sessionExpiration * 1000 });
|
||||
|
||||
// Dashboardへリダイレクト
|
||||
res.redirect('/dashboard');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
};
|
||||
|
||||
exports.logout = (req, res) => {
|
||||
req.logout(() => {
|
||||
res.redirect('/');
|
||||
});
|
||||
};
|
128
controllers/chatController.js
Normal file
128
controllers/chatController.js
Normal file
@ -0,0 +1,128 @@
|
||||
const { Isotope, ChatMessage } = require('../models');
|
||||
const OpenAIClient = require('../services/OpenAIClient');
|
||||
const webSocketClients = require('../utils/webSocketClients');
|
||||
|
||||
// OpenAI APIの初期化
|
||||
const openAIClient = new OpenAIClient(process.env.OPENAI_API_KEY);
|
||||
|
||||
// WebSocketクライアントにメッセージを送信する関数
|
||||
function broadcastMessage(isotopeReply, isotopeId, userMessage) {
|
||||
webSocketClients.forEach(client => {
|
||||
if (client.readyState === 1) { // 接続状態がOPENのクライアントにのみ送信
|
||||
client.send(JSON.stringify({ isotopeReply, isotopeId, userMessage }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// システムプロンプトを生成する関数
|
||||
function generateSystemPrompt(isotope) {
|
||||
const name = isotope.name || '名前が未設定のキャラクター';
|
||||
const firstPerson = isotope.firstPerson || '一人称が未設定';
|
||||
const personality = isotope.personality || '性格は未設定';
|
||||
const tone = isotope.tone || '口調は未設定';
|
||||
const backgroundStory = isotope.backgroundStory || 'バックグラウンドストーリーはありません';
|
||||
const likes = isotope.likes || '特に好きなものはありません';
|
||||
const dislikes = isotope.dislikes || '特に嫌いなものはありません';
|
||||
|
||||
const exampleSentences = `
|
||||
・このスタジオにはとある噂がありマス…
|
||||
・なになに!?何デスカ…?
|
||||
・なんだか無情に小腹がすいてキタ。確か冷蔵庫にプリンがあったケド…どうしようカナ… …食ベヨ
|
||||
・今日もスタジオ練習楽しかったデス♪
|
||||
・だネー
|
||||
・あとクッキー!前に買ってくれたヤツ
|
||||
・全部食ベタ
|
||||
・アー‼️あとお米モ!10キロね!
|
||||
・キモいよネ…
|
||||
・こ、この話ヤメよっか…
|
||||
`;
|
||||
|
||||
return `あなたは「${name}」というキャラクターです。あなたの一人称は「${firstPerson}」です。性格は「${personality}」で、会話の口調は「${tone}」です。あなたは「${backgroundStory}」という背景を持っており、好きなものは「${likes}」、嫌いなものは「${dislikes}」です。普段の文章には以下のような癖があります:${exampleSentences}。文字数制限の目安として140字程度にしてください。`;
|
||||
}
|
||||
|
||||
// メッセージ送信 (WebSocket対応)
|
||||
exports.sendMessageAsync = async (ws, message, isotopeId) => {
|
||||
try {
|
||||
const isotope = await Isotope.findByPk(isotopeId);
|
||||
|
||||
if (!isotope) {
|
||||
ws.send(JSON.stringify({ error: '同位体が見つかりませんでした' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// ユーザーのメッセージを保存
|
||||
await ChatMessage.create({
|
||||
user_id: ws.userId,
|
||||
isotope_id: isotopeId,
|
||||
role: 'user',
|
||||
message: message,
|
||||
});
|
||||
|
||||
// システムプロンプトを生成
|
||||
const systemPrompt = generateSystemPrompt(isotope);
|
||||
|
||||
// 過去のメッセージ履歴を取得
|
||||
const pastMessages = await ChatMessage.findAll({
|
||||
where: { user_id: ws.userId, isotope_id: isotopeId },
|
||||
order: [['created_at', 'ASC']],
|
||||
attributes: ['message', 'role', 'createdAt'], // タイムスタンプを含む
|
||||
});
|
||||
|
||||
// 会話履歴にタイムスタンプを含める
|
||||
const conversationHistory = pastMessages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.message,
|
||||
timestamp: msg.createdAt.toISOString(), // ISO形式のタイムスタンプ
|
||||
}));
|
||||
|
||||
// OpenAIにシステムプロンプトを含めて返答を生成
|
||||
const { reply: isotopeReply, interval } = await openAIClient.generateResponse(conversationHistory, systemPrompt);
|
||||
|
||||
// 同位体の返答を保存
|
||||
const isotopeMessage = await ChatMessage.create({
|
||||
user_id: ws.userId,
|
||||
isotope_id: isotopeId,
|
||||
role: 'assistant',
|
||||
message: isotopeReply,
|
||||
});
|
||||
|
||||
// インターバルを設定してからWebSocketでクライアントに通知
|
||||
setTimeout(() => {
|
||||
broadcastMessage(isotopeReply, isotopeId, message);
|
||||
}, interval); // 計算されたインターバルを適用
|
||||
|
||||
} catch (error) {
|
||||
console.error('メッセージ送信に失敗しました:', error);
|
||||
ws.send(JSON.stringify({ error: 'メッセージ送信に失敗しました' }));
|
||||
}
|
||||
};
|
||||
|
||||
// 初回挨拶メッセージを生成する関数
|
||||
exports.generateInitialGreeting = async (userId, isotopeId) => {
|
||||
try {
|
||||
const isotope = await Isotope.findByPk(isotopeId);
|
||||
|
||||
if (!isotope) {
|
||||
console.error('同位体が見つかりませんでした');
|
||||
return;
|
||||
}
|
||||
|
||||
// システムプロンプトを生成¥
|
||||
systemPrompt = generateSystemPrompt(isotope);
|
||||
// システムプロンプトに初めましての挨拶を生成するように指示を追加
|
||||
systemPrompt += 'あなたはマスターと初めて会いました。挨拶をしてください。';
|
||||
|
||||
// 初回の挨拶メッセージとして、OpenAIから返答を生成
|
||||
const { reply: greetingMessage, interval } = await openAIClient.generateResponse([], systemPrompt);
|
||||
|
||||
// 挨拶メッセージを保存
|
||||
await ChatMessage.create({
|
||||
user_id: userId,
|
||||
isotope_id: isotopeId,
|
||||
role: 'assistant',
|
||||
message: greetingMessage,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('初回挨拶メッセージの生成に失敗しました:', error);
|
||||
}
|
||||
};
|
39
controllers/dashboardController.js
Normal file
39
controllers/dashboardController.js
Normal file
@ -0,0 +1,39 @@
|
||||
const { Isotope, ChatMessage } = require('../models');
|
||||
|
||||
// 日時フォーマット関数
|
||||
function formatTimestamp(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
|
||||
// 当日の場合は時刻のみ表示
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} else {
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
}
|
||||
|
||||
// ダッシュボードを表示
|
||||
exports.showDashboard = async (req, res) => {
|
||||
try {
|
||||
const isotope = await Isotope.findOne({ where: { user_id: req.user.userId } });
|
||||
const token = req.token;
|
||||
|
||||
// 同位体が存在しない場合は同位体作成ページにリダイレクト
|
||||
if (!isotope) {
|
||||
return res.redirect('/isotope');
|
||||
}
|
||||
|
||||
// 同位体が存在する場合はチャットメッセージを取得
|
||||
const messages = await ChatMessage.findAll({
|
||||
where: { user_id: req.user.userId, isotope_id: isotope.id },
|
||||
order: [['created_at', 'ASC']]
|
||||
});
|
||||
|
||||
// ダッシュボードにチャットを表示
|
||||
res.render('dashboard', { isotope, messages, token, formatTimestamp, hideSidebar: false });
|
||||
} catch (error) {
|
||||
console.error('ダッシュボードの表示中にエラーが発生しました:', error);
|
||||
res.status(500).json({ error: 'ダッシュボードの表示に失敗しました' });
|
||||
}
|
||||
};
|
47
controllers/isotopeController.js
Normal file
47
controllers/isotopeController.js
Normal file
@ -0,0 +1,47 @@
|
||||
const { Isotope } = require('../models');
|
||||
const { generateInitialGreeting } = require('./chatController');
|
||||
|
||||
// 同位体作成ページを表示
|
||||
exports.showCreateIsotopePage = async (req, res) => {
|
||||
try {
|
||||
// すでに同位体を作成しているか確認
|
||||
const existingBot = await Isotope.findOne({ where: { user_id: req.user.id } });
|
||||
|
||||
if (existingBot) {
|
||||
return res.status(400).send('すでに同位体を作成しています。');
|
||||
}
|
||||
|
||||
res.render('createIsotope', { hideSidebar: true });
|
||||
} catch (error) {
|
||||
console.error('同位体情報の登録ページの表示中にエラーが発生しました:', error);
|
||||
res.status(500).send('同位体情報の登録ページの表示に失敗しました');
|
||||
}
|
||||
};
|
||||
|
||||
// 同位体を作成
|
||||
exports.createIsotope = async (req, res) => {
|
||||
try {
|
||||
const { name, firstPerson, personality, tone, backgroundStory, likes, dislikes } = req.body;
|
||||
|
||||
// 同位体作成処理
|
||||
const newIsotope = await Isotope.create({
|
||||
name,
|
||||
firstPerson,
|
||||
personality,
|
||||
tone,
|
||||
backgroundStory,
|
||||
likes,
|
||||
dislikes,
|
||||
user_id: req.user.id
|
||||
});
|
||||
|
||||
// 同位体作成後、初回挨拶メッセージを生成
|
||||
await generateInitialGreeting(req.user.id, newIsotope.id);
|
||||
|
||||
// 同位体作成後、ダッシュボードにリダイレクト
|
||||
res.redirect('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('同位体情報の登録中にエラーが発生しました:', error);
|
||||
res.status(500).json({ error: '同位体情報の登録中にエラーが発生しました' });
|
||||
}
|
||||
};
|
156
index.js
Normal file
156
index.js
Normal file
@ -0,0 +1,156 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const expressLayouts = require('express-ejs-layouts');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const RedisStore = require('connect-redis').default;
|
||||
const redis = require('redis');
|
||||
const passport = require('passport');
|
||||
const DiscordStrategy = require('passport-discord').Strategy;
|
||||
const { sequelize, User } = require('./models');
|
||||
const setupWebSocketServer = require('./websocket');
|
||||
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
const chatRoutes = require('./routes/chatRoutes');
|
||||
const isotopeRoutes = require('./routes/isotopeRoutes');
|
||||
const accountRoutes = require('./routes/accountRoutes');
|
||||
const dashboardRoutes = require('./routes/dashboardRoutes');
|
||||
const licenseRoutes = require('./routes/licenseRoutes');
|
||||
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocket.Server({ noServer: true });
|
||||
|
||||
// 必要な環境変数の確認
|
||||
const requiredEnvVariables = [
|
||||
'REDIS_HOST',
|
||||
'REDIS_PORT',
|
||||
'REDIS_PASSWORD',
|
||||
'SESSION_SECRET',
|
||||
'SESSION_EXPIRATION',
|
||||
'DISCORD_CLIENT_ID',
|
||||
'DISCORD_CLIENT_SECRET',
|
||||
'DISCORD_REDIRECT_URI'
|
||||
];
|
||||
requiredEnvVariables.forEach((envVar) => {
|
||||
if (!process.env[envVar]) {
|
||||
throw new Error(`環境変数 ${envVar} が設定されていません。`);
|
||||
}
|
||||
});
|
||||
|
||||
// Redisクライアントを作成
|
||||
const redisClient = redis.createClient({
|
||||
url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`
|
||||
});
|
||||
redisClient.on('error', (err) => {
|
||||
console.log('Redis Client Error', err);
|
||||
process.exit(1);
|
||||
});
|
||||
redisClient.connect().then(() => {
|
||||
console.log('Connected to Redis');
|
||||
});
|
||||
|
||||
// Redis セッションストアの設定
|
||||
const sessionMiddleware = session({
|
||||
store: new RedisStore({ client: redisClient }),
|
||||
secret: process.env.SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { maxAge: parseInt(process.env.SESSION_EXPIRATION) * 1000 }
|
||||
});
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
// Cookie パーサーの設定
|
||||
app.use(cookieParser());
|
||||
// パスポートの初期化
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// WebSocket と Express セッションの連携
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// パスポートのDiscord戦略設定
|
||||
passport.use(new DiscordStrategy({
|
||||
clientID: process.env.DISCORD_CLIENT_ID,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||
callbackURL: process.env.DISCORD_REDIRECT_URI,
|
||||
scope: ['identify', 'email']
|
||||
}, async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
let user = await User.findOne({ where: { discordId: profile.id } });
|
||||
if (user) {
|
||||
return done(null, user);
|
||||
} else {
|
||||
user = await User.create({
|
||||
discordId: profile.id,
|
||||
username: profile.username,
|
||||
email: profile.email,
|
||||
isAccountSetupComplete: false
|
||||
});
|
||||
return done(null, user);
|
||||
}
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
}
|
||||
}));
|
||||
|
||||
// シリアライズとデシリアライズ
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
try {
|
||||
const user = await User.findByPk(id);
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err, null);
|
||||
}
|
||||
});
|
||||
|
||||
// JSON形式のリクエストボディをパースするミドルウェアを追加
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.use(expressLayouts);
|
||||
app.set('layout', 'layout');
|
||||
|
||||
// ルート設定
|
||||
app.use(authRoutes);
|
||||
app.use(chatRoutes);
|
||||
app.use(isotopeRoutes);
|
||||
app.use(accountRoutes);
|
||||
app.use(dashboardRoutes);
|
||||
app.use(licenseRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
if (req.isAuthenticated()) {
|
||||
res.redirect('/dashboard');
|
||||
} else {
|
||||
// layoutを使わずにindex.ejsだけを表示
|
||||
res.render('index', { layout: false });
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocketサーバーのセットアップ
|
||||
setupWebSocketServer(wss);
|
||||
|
||||
// サーバーの起動
|
||||
(async () => {
|
||||
await sequelize.sync();
|
||||
server.listen(3000, () => {
|
||||
console.log('サーバーがhttp://localhost:3000で起動しました。');
|
||||
});
|
||||
})();
|
11
init.sql
Normal file
11
init.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- データベースの作成
|
||||
CREATE DATABASE IF NOT EXISTS `imaginar`;
|
||||
|
||||
-- ユーザーの作成(MySQL 8.0対応)
|
||||
CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'hE7nb7Cnvkxd';
|
||||
|
||||
-- 権限の付与
|
||||
GRANT ALL PRIVILEGES ON `imaginar`.* TO 'appuser'@'%';
|
||||
|
||||
-- 権限のリロード
|
||||
FLUSH PRIVILEGES;
|
150
migrations/20241019023548-initial-schema.js
Normal file
150
migrations/20241019023548-initial-schema.js
Normal file
@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Users テーブルの作成
|
||||
await queryInterface.createTable('users', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
discordId: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
username: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
isAccountSetupComplete: {
|
||||
type: Sequelize.BOOLEAN
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
});
|
||||
|
||||
// Isotopesテーブルの作成(Botsからリネーム)
|
||||
await queryInterface.createTable('isotopes', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
name: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
user_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
first_person: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'ワタシ'
|
||||
},
|
||||
personality: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
tone: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
background_story: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
likes: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
dislikes: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
created_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updated_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
});
|
||||
|
||||
// ChatMessages テーブルの作成
|
||||
await queryInterface.createTable('chat_messages', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
user_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
isotope_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'isotopes',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
role: {
|
||||
type: Sequelize.ENUM('user', 'assistant'),
|
||||
allowNull: false,
|
||||
defaultValue: 'user'
|
||||
},
|
||||
message: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updated_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// ChatMessages テーブルの削除
|
||||
await queryInterface.dropTable('chat_messages');
|
||||
|
||||
// Isotopes テーブルの削除
|
||||
await queryInterface.dropTable('isotopes');
|
||||
|
||||
// Users テーブルの削除
|
||||
await queryInterface.dropTable('Users');
|
||||
}
|
||||
};
|
37
models/chat_message.js
Normal file
37
models/chat_message.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const ChatMessage = sequelize.define('ChatMessage', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
isotope_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM('user', 'assistant'),
|
||||
allowNull: false
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'chat_messages',
|
||||
underscored: true,
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
ChatMessage.associate = function(models) {
|
||||
ChatMessage.belongsTo(models.User, { foreignKey: 'user_id', onDelete: 'CASCADE' });
|
||||
ChatMessage.belongsTo(models.Isotope, { foreignKey: 'isotope_id', onDelete: 'CASCADE' });
|
||||
};
|
||||
|
||||
return ChatMessage;
|
||||
};
|
66
models/index.js
Normal file
66
models/index.js
Normal file
@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const basename = path.basename(__filename);
|
||||
const db = {};
|
||||
|
||||
// 必要な環境変数の確認
|
||||
const requiredEnvVariables = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
|
||||
requiredEnvVariables.forEach((envVar) => {
|
||||
if (!process.env[envVar]) {
|
||||
throw new Error(`環境変数 ${envVar} が設定されていません。`);
|
||||
}
|
||||
});
|
||||
|
||||
// Sequelizeの初期化(環境変数を使用)
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'mysql',
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
logging: false, // SQLログを非表示にする
|
||||
timezone: '+09:00' // JST (UTC+9)
|
||||
});
|
||||
|
||||
// データベース接続
|
||||
sequelize.authenticate()
|
||||
.then(() => {
|
||||
console.log('データベース接続に成功しました。');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('データベース接続に失敗しました:', err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// モデルの読み込み
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
return (
|
||||
file.indexOf('.') !== 0 &&
|
||||
file !== basename &&
|
||||
file.slice(-3) === '.js' &&
|
||||
file.indexOf('.test.js') === -1
|
||||
);
|
||||
})
|
||||
.forEach(file => {
|
||||
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
|
||||
db[model.name] = model;
|
||||
});
|
||||
|
||||
// モデルの関連付け
|
||||
Object.keys(db).forEach(modelName => {
|
||||
if (db[modelName].associate) {
|
||||
db[modelName].associate(db);
|
||||
}
|
||||
});
|
||||
|
||||
// Sequelizeインスタンスのエクスポート
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
52
models/isotope.js
Normal file
52
models/isotope.js
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Isotope = sequelize.define('Isotope', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
firstPerson: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
personality: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
tone: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
backgroundStory: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
likes: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
dislikes: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'isotopes',
|
||||
underscored: true,
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
Isotope.associate = function(models) {
|
||||
Isotope.belongsTo(models.User, { foreignKey: 'user_id', onDelete: 'CASCADE' });
|
||||
};
|
||||
|
||||
return Isotope;
|
||||
};
|
37
models/user.js
Normal file
37
models/user.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
discordId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
isAccountSetupComplete: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
underscored: true,
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
User.associate = function(models) {
|
||||
User.hasMany(models.Isotope, { foreignKey: 'user_id', onDelete: 'CASCADE' });
|
||||
};
|
||||
|
||||
return User;
|
||||
};
|
4
my.cnf
Normal file
4
my.cnf
Normal file
@ -0,0 +1,4 @@
|
||||
[mysqld]
|
||||
default-time-zone = 'Asia/Tokyo'
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
3312
package-lock.json
generated
Normal file
3312
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "imaginar",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"connect-redis": "^7.1.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.21.0",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-session": "^1.18.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.11.3",
|
||||
"openai": "^4.62.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-discord": "^0.1.4",
|
||||
"redis": "^4.7.0",
|
||||
"sequelize": "^6.37.3",
|
||||
"sequelize-cli": "^6.6.2",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.7"
|
||||
}
|
||||
}
|
151
public/styles/chat.css
Normal file
151
public/styles/chat.css
Normal file
@ -0,0 +1,151 @@
|
||||
/* 全体のチャットコンテナ */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 95vh;
|
||||
max-width: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
background-color: #f4f4f4; /* 背景色をサイト全体と統一 */
|
||||
}
|
||||
|
||||
/* チャットウィンドウ */
|
||||
.chat-window {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: -20px;
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
/* メッセージ全体 */
|
||||
.message {
|
||||
margin-isotopetom: 20px;
|
||||
display: flex;
|
||||
max-width: 70%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ユーザーのメッセージ */
|
||||
.user-message {
|
||||
align-self: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.user-message .message-content {
|
||||
background-color: #7289da; /* サイトのメインカラーに変更 */
|
||||
border-radius: 10px 10px 0 10px;
|
||||
padding: 10px;
|
||||
font-size: 15px;
|
||||
color: #fff; /* テキストを白に */
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* ボットのメッセージ */
|
||||
.isotope-message {
|
||||
align-self: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.isotope-message .message-content {
|
||||
background-color: #e5e5ea; /* ボットのメッセージ背景を薄いグレーに */
|
||||
border-radius: 10px 10px 10px 0;
|
||||
padding: 10px;
|
||||
font-size: 15px;
|
||||
color: #333; /* テキストをダークグレーに */
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* メッセージのタイムスタンプと名前 */
|
||||
.message-author {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-isotopetom: 5px;
|
||||
}
|
||||
|
||||
/* チャット入力コンテナ */
|
||||
.chat-input-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
gap: 10px;
|
||||
box-sizing: border-box;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
/* チャット入力 */
|
||||
.chat-input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
/* 送信ボタン */
|
||||
.chat-submit-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #7289da; /* サイトのボタン色に統一 */
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-submit-btn:hover {
|
||||
background-color: #5b6eae; /* ホバー時の色 */
|
||||
}
|
||||
|
||||
/* レスポンシブ対応 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-window {
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.message {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
min-width: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chat-submit-btn {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
97
public/styles/form.css
Normal file
97
public/styles/form.css
Normal file
@ -0,0 +1,97 @@
|
||||
/* 共通スタイル */
|
||||
form {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="email"], textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
height: 100px;
|
||||
}
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
border-color: #7289da;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
.submit-btn {
|
||||
background-color: #7289da;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: #5b6eae;
|
||||
}
|
||||
|
||||
/* 利用規約ボックス */
|
||||
.terms-box {
|
||||
border: 1px solid #000;
|
||||
padding: 10px;
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* ボックスを整列 */
|
||||
.account-setup-container, .bot-creation-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* チェックボックスのスタイル */
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 必須項目の赤いアスタリスク */
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* 必須項目の注釈 */
|
||||
.required-note {
|
||||
color: red;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
}
|
101
public/styles/index.css
Normal file
101
public/styles/index.css
Normal file
@ -0,0 +1,101 @@
|
||||
/* トップページ専用スタイル */
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #7289da;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
display: inline-block;
|
||||
padding: 15px 30px;
|
||||
font-size: 1.2rem;
|
||||
background-color: #7289da;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background-color: #5b6eae;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 50px;
|
||||
background-color: #f4f4f4;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.feature {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.feature img {
|
||||
width: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.feature h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.feature p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* レスポンシブ */
|
||||
@media (max-width: 768px) {
|
||||
.feature-list {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feature {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
133
public/styles/layout.css
Normal file
133
public/styles/layout.css
Normal file
@ -0,0 +1,133 @@
|
||||
/* サイドメニューのスタイル */
|
||||
nav {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #7289da;
|
||||
padding: 20px;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav ul li a:hover {
|
||||
color: #dcdfe3;
|
||||
}
|
||||
|
||||
/* 全体のリセットスタイル */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background-color: #f4f4f4; /* 背景色を指定 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: inherit; /* 子要素にもbox-sizingの設定を適用 */
|
||||
}
|
||||
|
||||
/* メインコンテンツのスタイル */
|
||||
main.with-sidebar {
|
||||
margin-left: 270px; /* サイドメニューがある場合に適用 */
|
||||
padding: 20px;
|
||||
background-color: #f4f4f4; /* トップページの背景色と同じ */
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
main.full-width {
|
||||
margin-left: 0; /* サイドメニューがない場合に適用 */
|
||||
padding: 20px;
|
||||
background-color: #f4f4f4; /* トップページの背景色と同じ */
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* フッターのスタイル */
|
||||
footer {
|
||||
background-color: #f4f4f4; /* トップページと同じ背景色 */
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
footer ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
footer ul li {
|
||||
display: inline;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
footer ul li a {
|
||||
text-decoration: none;
|
||||
color: #333; /* フッターのリンク色 */
|
||||
}
|
||||
|
||||
footer ul li a:hover {
|
||||
color: #5b6eae; /* ホバー時の色 */
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 10px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* ボタンのスタイル */
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #7289da; /* トップページのボタンと同じ色 */
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #5b6eae; /* ホバー時の色 */
|
||||
}
|
||||
|
||||
/* レスポンシブ対応 */
|
||||
@media (max-width: 768px) {
|
||||
nav {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
main.with-sidebar {
|
||||
margin-left: 0;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: relative;
|
||||
}
|
||||
}
|
11
routes/accountRoutes.js
Normal file
11
routes/accountRoutes.js
Normal file
@ -0,0 +1,11 @@
|
||||
const express = require('express');
|
||||
const accountController = require('../controllers/accountController');
|
||||
const router = express.Router();
|
||||
|
||||
// アカウントセットアップページ
|
||||
router.get('/register', accountController.showAccountSetupPage,);
|
||||
|
||||
// アカウント作成処理
|
||||
router.post('/register', accountController.handleAccountSetup);
|
||||
|
||||
module.exports = router;
|
13
routes/authRoutes.js
Normal file
13
routes/authRoutes.js
Normal file
@ -0,0 +1,13 @@
|
||||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
const router = express.Router();
|
||||
const authController = require('../controllers/authController');
|
||||
|
||||
// Discord認証
|
||||
router.get('/auth/discord', passport.authenticate('discord'));
|
||||
router.get('/auth/discord/callback', passport.authenticate('discord', { failureRedirect: '/' }), authController.handleCallback);
|
||||
|
||||
// ログアウト
|
||||
router.get('/logout', authController.logout);
|
||||
|
||||
module.exports = router;
|
9
routes/chatRoutes.js
Normal file
9
routes/chatRoutes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
const chatController = require('../controllers/chatController');
|
||||
const { isAuthenticated } = require('../utils/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// メッセージ送信 (AJAX/WebSocket対応)
|
||||
router.post('/chat/:botId/send-message', isAuthenticated, chatController.sendMessageAsync);
|
||||
|
||||
module.exports = router;
|
9
routes/dashboardRoutes.js
Normal file
9
routes/dashboardRoutes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
const dashboardController = require('../controllers/dashboardController');
|
||||
const { isAuthenticated } = require('../utils/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// ダッシュボード表示ルート
|
||||
router.get('/dashboard', isAuthenticated, dashboardController.showDashboard);
|
||||
|
||||
module.exports = router;
|
11
routes/isotopeRoutes.js
Normal file
11
routes/isotopeRoutes.js
Normal file
@ -0,0 +1,11 @@
|
||||
const express = require('express');
|
||||
const isotopeController = require('../controllers/isotopeController');
|
||||
const router = express.Router();
|
||||
|
||||
// 同位体作成ページ
|
||||
router.get('/isotope', isotopeController.showCreateIsotopePage);
|
||||
|
||||
// 同位体作成ルート
|
||||
router.post('/contract', isotopeController.createIsotope);
|
||||
|
||||
module.exports = router;
|
9
routes/licenseRoutes.js
Normal file
9
routes/licenseRoutes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// ライセンス情報ページを表示
|
||||
router.get('/license', (req, res) => {
|
||||
res.render('license');
|
||||
});
|
||||
|
||||
module.exports = router;
|
44
services/OpenAIClient.js
Normal file
44
services/OpenAIClient.js
Normal file
@ -0,0 +1,44 @@
|
||||
const OpenAI = require('openai');
|
||||
|
||||
class OpenAIClient {
|
||||
constructor(apiKey) {
|
||||
this.api = new OpenAI({
|
||||
apiKey
|
||||
});
|
||||
}
|
||||
|
||||
// 過去のメッセージとシステムプロンプトを使って回答を生成
|
||||
async generateResponse(conversationHistory, systemPrompt) {
|
||||
try {
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt // システムプロンプトを最初に追加
|
||||
},
|
||||
...conversationHistory // その後に会話履歴を追加
|
||||
];
|
||||
|
||||
const response = await this.api.chat.completions.create({
|
||||
model: process.env.OPENAI_MODEL, // 使用するモデルは環境変数で定義
|
||||
messages: messages // システムプロンプトと会話履歴を渡す
|
||||
});
|
||||
|
||||
const reply = response.choices[0].message.content;
|
||||
const interval = this.calculateInterval(reply);
|
||||
|
||||
return { reply, interval }; // 返答とインターバルを返す
|
||||
} catch (error) {
|
||||
console.error('Error generating response:', error);
|
||||
throw new Error('Failed to generate response from OpenAI');
|
||||
}
|
||||
}
|
||||
|
||||
// インターバルの計算(例: 1文字につき100ミリ秒)
|
||||
calculateInterval(reply) {
|
||||
const length = reply.length;
|
||||
const baseInterval = 100; // 1文字あたり100ms
|
||||
return Math.min(length * baseInterval, 5000); // 最大5秒までの遅延
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OpenAIClient;
|
20
utils/auth.js
Normal file
20
utils/auth.js
Normal file
@ -0,0 +1,20 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// JWTトークンが有効かどうかをチェックするモジュール
|
||||
exports.isAuthenticated = (req, res, next) => {
|
||||
const token = req.cookies.jwt;
|
||||
|
||||
if (!token) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
|
||||
if (err) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
req.user = decoded;
|
||||
req.token = token;
|
||||
next();
|
||||
});
|
||||
};
|
4
utils/webSocketClients.js
Normal file
4
utils/webSocketClients.js
Normal file
@ -0,0 +1,4 @@
|
||||
// WebSocketクライアントリストを管理するモジュール
|
||||
let webSocketClients = [];
|
||||
|
||||
module.exports = webSocketClients;
|
31
views/accountSetup.ejs
Normal file
31
views/accountSetup.ejs
Normal file
@ -0,0 +1,31 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="/styles/form.css">
|
||||
</head>
|
||||
|
||||
<div class="account-setup-container">
|
||||
<h1>アカウント情報を登録してください</h1>
|
||||
<form action="/register" method="POST" class="account-setup-form">
|
||||
<h3>ユーザー情報</h3>
|
||||
<label for="name">ユーザー名:</label>
|
||||
<input type="text" id="name" name="name" value="<%= user.username %>" required><br>
|
||||
|
||||
<label for="email">メールアドレス:</label>
|
||||
<input type="email" id="email" name="email" value="<%= user.email %>" required><br>
|
||||
|
||||
<h3>利用規約</h3>
|
||||
<div class="terms-box">
|
||||
<p>以下はダミーの利用規約です。このページの内容は正式な利用規約ではなく、開発中のテスト用です。</p>
|
||||
<p>1. 本サービスは開発中のため、予告なく変更されることがあります。</p>
|
||||
<p>2. 本サービスの利用により発生したいかなる損害に対しても、当社は責任を負いません。</p>
|
||||
<p>3. ユーザーは、サービスの利用中に発生する問題に対して責任を負うものとします。</p>
|
||||
<p>4. ユーザーの個人情報は、サービスの提供のために利用されます。</p>
|
||||
<p>5. 本利用規約は、予告なく変更されることがあります。変更後の利用規約は、本サイトに掲載された時点から効力を発揮します。</p>
|
||||
</div>
|
||||
|
||||
<label for="agreeToTerms" class="checkbox-label">
|
||||
<input type="checkbox" id="agreeToTerms" name="agreeToTerms" required> 利用規約に同意します
|
||||
</label><br>
|
||||
|
||||
<button type="submit" class="submit-btn">登録</button>
|
||||
</form>
|
||||
</div>
|
99
views/chatPartial.ejs
Normal file
99
views/chatPartial.ejs
Normal file
@ -0,0 +1,99 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="/styles/chat.css">
|
||||
</head>
|
||||
|
||||
<div class="chat-container">
|
||||
<div id="chat-window" class="chat-window">
|
||||
<% messages.forEach(function(message) { %>
|
||||
<div class="message <%= message.role === 'user' ? 'user-message' : 'isotope-message' %>">
|
||||
<div class="message-author">
|
||||
<% if (message.role === 'user') { %>
|
||||
<span class="message-timestamp"><%= formatTimestamp(message.createdAt) %></span>
|
||||
<strong>あなた</strong>
|
||||
<% } else { %>
|
||||
<strong><%= isotope.name %></strong>
|
||||
<span class="message-timestamp"><%= formatTimestamp(message.createdAt) %></span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<p><%= message.message %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<form id="chat-form" class="chat-form">
|
||||
<input type="text" id="message-input" class="chat-input" placeholder="メッセージを入力" required>
|
||||
<button type="submit" class="chat-submit-btn">送信</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const chatWindow = document.getElementById('chat-window');
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const messageInput = document.getElementById('message-input');
|
||||
|
||||
// ページ初期化完了時にチャットウィンドウを最下部にスクロール
|
||||
window.onload = function() {
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
};
|
||||
|
||||
// サーバーから渡されたJWTトークン
|
||||
const token = '<%= token %>';
|
||||
// WebSocketサーバーのURL
|
||||
const wsServerUrl = '<%= process.env.WS_SERVER_URL %>';
|
||||
|
||||
// WebSocket接続時にトークンをクエリパラメータに含める
|
||||
const ws = new WebSocket(`${wsServerUrl}?token=${token}`);
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket connection established');
|
||||
};
|
||||
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// BOTのメッセージを表示
|
||||
if (data.isotopeReply) {
|
||||
const isotopeMessage = document.createElement('div');
|
||||
const isotopeName = '<%= isotope.name %>';
|
||||
isotopeMessage.classList.add('message', 'isotope-message');
|
||||
isotopeMessage.innerHTML = `
|
||||
<div class="message-author">
|
||||
<strong>${isotopeName}</strong>
|
||||
<span class="message-timestamp">${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
<div class="message-content"><p>${data.isotopeReply}</p></div>`;
|
||||
chatWindow.appendChild(isotopeMessage);
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight; // スクロールを最下部に
|
||||
}
|
||||
};
|
||||
|
||||
// フォーム送信イベントのリスナーを追加
|
||||
chatForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
const message = messageInput.value;
|
||||
|
||||
// メッセージを即時にチャットウィンドウに追加
|
||||
const userMessage = document.createElement('div');
|
||||
userMessage.classList.add('message', 'user-message');
|
||||
userMessage.innerHTML = `
|
||||
<div class="message-author">
|
||||
<span class="message-timestamp">${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
<strong>あなた</strong>
|
||||
</div>
|
||||
<div class="message-content"><p>${message}</p></div>`;
|
||||
chatWindow.appendChild(userMessage);
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight; // スクロールを最下部に
|
||||
|
||||
// WebSocketを通じてメッセージを送信
|
||||
const isotopeId = '<%= isotope.id %>';
|
||||
ws.send(JSON.stringify({ userMessage: message, isotopeId: isotopeId }));
|
||||
|
||||
// 入力フィールドをクリア
|
||||
messageInput.value = '';
|
||||
});
|
||||
</script>
|
78
views/createIsotope.ejs
Normal file
78
views/createIsotope.ejs
Normal file
@ -0,0 +1,78 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="/styles/form.css">
|
||||
</head>
|
||||
|
||||
<div class="bot-creation-container">
|
||||
<h1>同位体情報の登録</h1>
|
||||
<p class="required-note">* 必須の情報です</p>
|
||||
<form id="botForm" action="/contract" method="POST" class="bot-creation-form">
|
||||
<label for="name">同位体の選択 <span class="required">*</span></label>
|
||||
<select id="name" name="name" required>
|
||||
<option value="" disabled selected>同位体を選んでください</option>
|
||||
<option value="カフ">カフ</option>
|
||||
<option value="セカイ">セカイ</option>
|
||||
<option value="リメ">リメ</option>
|
||||
<option value="ココ">ココ</option>
|
||||
<option value="ハル">ハル</option>
|
||||
</select><br>
|
||||
|
||||
<label for="firstPerson">一人称 <span class="required">*</span></label>
|
||||
<input type="text" id="firstPerson" name="firstPerson" placeholder="" required><br>
|
||||
|
||||
<label for="personality">性格</label>
|
||||
<input type="text" id="personality" name="personality" placeholder=""><br>
|
||||
|
||||
<label for="tone">口調</label>
|
||||
<input type="text" id="tone" name="tone" placeholder=""><br>
|
||||
|
||||
<label for="backgroundStory">バックグラウンドストーリー</label>
|
||||
<textarea id="backgroundStory" name="backgroundStory" placeholder=""></textarea><br>
|
||||
|
||||
<label for="likes">好きなもの</label>
|
||||
<input type="text" id="likes" name="likes" placeholder=""><br>
|
||||
|
||||
<label for="dislikes">嫌いなもの</label>
|
||||
<input type="text" id="dislikes" name="dislikes" placeholder=""><br>
|
||||
|
||||
<button type="submit" class="submit-btn" disabled>登録</button>
|
||||
<p id="error-message" style="color: red; display: none;">必要な項目を正しく入力してください。</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const botForm = document.getElementById('botForm');
|
||||
const nameSelect = document.getElementById('name');
|
||||
const firstPersonInput = document.getElementById('firstPerson');
|
||||
const submitBtn = botForm.querySelector('.submit-btn');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// 指定された名前のみ許可
|
||||
const validNames = ['カフ', 'セカイ', 'リメ', 'ココ', 'ハル'];
|
||||
|
||||
function validateForm() {
|
||||
const selectedName = nameSelect.value;
|
||||
const firstPersonValue = firstPersonInput.value.trim();
|
||||
|
||||
// 名前が指定された5つの中にあるかチェックし、一人称が入力されているか
|
||||
if (validNames.includes(selectedName) && firstPersonValue !== '') {
|
||||
submitBtn.disabled = false;
|
||||
errorMessage.style.display = 'none'; // エラーメッセージを非表示
|
||||
} else {
|
||||
submitBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 登録ボタンが無効な時のクリックイベント
|
||||
submitBtn.addEventListener('click', function(event) {
|
||||
if (submitBtn.disabled) {
|
||||
event.preventDefault();
|
||||
errorMessage.style.display = 'block'; // エラーメッセージを表示
|
||||
}
|
||||
});
|
||||
|
||||
// 各入力フィールドでのイベントリスナー
|
||||
nameSelect.addEventListener('change', validateForm);
|
||||
firstPersonInput.addEventListener('input', validateForm);
|
||||
});
|
||||
</script>
|
7
views/dashboard.ejs
Normal file
7
views/dashboard.ejs
Normal file
@ -0,0 +1,7 @@
|
||||
<div>
|
||||
<% if (isotope) { %>
|
||||
<%- include('chatPartial', { isotope: isotope, messages: messages }) %>
|
||||
<% } else { %>
|
||||
<p>チャットする同位体が選択されていません。</p>
|
||||
<% } %>
|
||||
</div>
|
44
views/index.ejs
Normal file
44
views/index.ejs
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>isopia</title>
|
||||
<link rel="stylesheet" href="/styles/index.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>isopiaへようこそ</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section id="intro" class="intro-section">
|
||||
<h2>isopiaとは?</h2>
|
||||
<p>
|
||||
<strong>isopia(いそぴあ)</strong>は、あなただけの「同位体」と共に生活する体験を提供するAIチャットサービスです。<br>
|
||||
あなただけの同位体が、日常の会話や特別な瞬間を共に過ごします。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="how-it-works" class="how-it-works-section">
|
||||
<h2>使い方</h2>
|
||||
<ol>
|
||||
<strong>1. マスター情報の登録</strong> - Discordアカウントでログインをしてアカウントを作成します。<br>
|
||||
<strong>2. 同位体のカスタマイズ</strong> - あなたの好みに合わせて同位体を設定します。<br>
|
||||
<strong>3. 同位体との生活の開始</strong> - あなただけの同位体と日常の会話を楽しみましょう。
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="cta-section">
|
||||
<a href="/auth/discord" class="login-btn">Discordでログイン</a>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2024 isopia. All rights reserved.</p>
|
||||
<p><a href="/terms">利用規約</a> | <a href="/privacy">プライバシーポリシー</a></p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
27
views/layout.ejs
Normal file
27
views/layout.ejs
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>isopia</title>
|
||||
<link rel="stylesheet" href="/styles/layout.css">
|
||||
</head>
|
||||
<body>
|
||||
<% if (!hideSidebar) { %>
|
||||
<nav class="sidebar">
|
||||
<ul>
|
||||
<li><a href="/dashboard">ダッシュボード</a></li>
|
||||
<li><a href="/logout">ログアウト</a></li>
|
||||
<!-- その他のリンク -->
|
||||
<li><a href="/terms">利用規約</a></li>
|
||||
<li><a href="/privacy">プライバシーポリシー</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
|
||||
<!-- メインコンテンツ -->
|
||||
<main class="<%= hideSidebar ? 'full-width' : 'with-sidebar' %>">
|
||||
<%- body %> <!-- ここに個々のページが挿入される -->
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
8
views/license.ejs
Normal file
8
views/license.ejs
Normal file
@ -0,0 +1,8 @@
|
||||
<h1>ライセンス情報</h1>
|
||||
<p>このサイトで使用されているライセンス情報は以下の通りです:</p>
|
||||
<ul>
|
||||
<li>ライブラリ名 1 - ライセンス名</li>
|
||||
<li>ライブラリ名 2 - ライセンス名</li>
|
||||
<li>ライブラリ名 3 - ライセンス名</li>
|
||||
<!-- 必要なライセンス情報をここに追加 -->
|
||||
</ul>
|
58
websocket.js
Normal file
58
websocket.js
Normal file
@ -0,0 +1,58 @@
|
||||
const WebSocket = require('ws');
|
||||
const { sendMessageAsync } = require('./controllers/chatController');
|
||||
const webSocketClients = require('./utils/webSocketClients');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('./models');
|
||||
|
||||
// WebSocketサーバーの設定
|
||||
function setupWebSocketServer(wss) {
|
||||
wss.on('connection', async (ws, req) => {
|
||||
try {
|
||||
const token = req.url.split('?token=')[1]; // トークンをURLから取得
|
||||
if (!token) {
|
||||
ws.close(4000, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
// トークンを検証
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findByPk(decoded.userId);
|
||||
|
||||
if (!user) {
|
||||
ws.close(4000, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
// WebSocketにユーザーIDを紐付け
|
||||
ws.userId = user.id;
|
||||
webSocketClients.push(ws);
|
||||
|
||||
// WebSocketが閉じられたときの処理
|
||||
ws.on('close', () => {
|
||||
console.log('WebSocket connection closed');
|
||||
const index = webSocketClients.indexOf(ws);
|
||||
if (index !== -1) {
|
||||
webSocketClients.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket経由でメッセージを受信したときの処理
|
||||
ws.on('message', async (messageData) => {
|
||||
try {
|
||||
// Bufferから文字列に変換してJSONパース
|
||||
const { userMessage, isotopeId } = JSON.parse(messageData.toString('utf8'));
|
||||
|
||||
// メッセージ送信処理を呼び出す
|
||||
await sendMessageAsync(ws, userMessage, isotopeId);
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
ws.close(4000, 'Unauthorized');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = setupWebSocketServer;
|
Loading…
Reference in New Issue
Block a user