Complete migration from OpenAI Assistants API to Chat Completions API with Vue.js frontend

Major Features Implemented:
- Full Vue.js 3 admin interface with Vite build system
- OpenAI Chat Completions API integration (replaced deprecated Assistants API)
- PostgreSQL database with Sequelize ORM
- Complete conversation management system
- User authentication system (admin@oliver.agency, user@oliver.agency)
- AI-powered conversation title generation
- Server-Sent Events for streaming responses
- Conversation soft delete functionality
- Rate limiting middleware with development bypass

Backend Infrastructure:
- Node.js/Express server with comprehensive error handling
- Database models: User, Assistant, Conversation, Message
- Chat API endpoints with full conversation history context
- Conversation CRUD operations with soft delete
- Migration and seeding scripts
- Environment-based configuration

Frontend Features:
- Responsive Vue.js interface with router
- Real-time chat with streaming responses
- Conversation sidebar with delete functionality
- Agent selection dropdown
- Persistent user sessions with hash-based user IDs
- Conversation history loading and continuity
- Login system with user role management
- Prominent logout functionality

Technical Improvements:
- Fixed conversation continuity by loading full message history
- Implemented conversation title generation using GPT-4o-mini
- Added conversation persistence mechanisms (periodic refresh, window focus)
- Enhanced error handling and rate limiting
- Proper environment variable management
- Clean project structure with separated concerns

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DJP 2025-09-03 13:08:26 -04:00
parent aec2fe691c
commit 88d18619bb
28 changed files with 5061 additions and 18 deletions

99
.gitignore vendored Normal file
View file

@ -0,0 +1,99 @@
# Dependencies
node_modules/
*/node_modules/
admin/node_modules/
server/node_modules/
# Environment files
.env
*.env.local
*.env.development
*.env.test
*.env.production
# Build outputs
dist/
build/
*/dist/
*/build/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
server.log
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# 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 variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/

13
admin/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ideas Generator 2025</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1473
admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
admin/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "ideas-generator-2025-admin",
"version": "1.0.0",
"description": "Frontend for Ideas Generator 2025",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"serve": "vite preview --port 8080"
},
"keywords": ["vue", "vite", "ideas-generator"],
"author": "",
"license": "ISC",
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.5",
"axios": "^1.6.0",
"marked": "^11.1.1",
"highlight.js": "^11.9.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"vite": "^5.0.10"
}
}

74
admin/src/App.vue Normal file
View file

@ -0,0 +1,74 @@
<template>
<div id="app">
<nav class="navbar">
<div class="nav-brand">
<router-link to="/" class="brand-link">
<h1>🚀 Ideas Generator 2025</h1>
</router-link>
</div>
<div class="nav-links">
<router-link to="/" class="nav-link">Home</router-link>
<router-link to="/chat" class="nav-link">Chat</router-link>
</div>
</nav>
<main class="main-content">
<router-view />
</main>
</div>
</template>
<style scoped>
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.brand-link {
color: white;
text-decoration: none;
}
.brand-link h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
.nav-links {
display: flex;
gap: 2rem;
}
.nav-link {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: background-color 0.2s;
}
.nav-link:hover,
.nav-link.router-link-active {
background-color: rgba(255,255,255,0.2);
}
.main-content {
flex: 1;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
#app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>

36
admin/src/main.js Normal file
View file

@ -0,0 +1,36 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import Home from './pages/Home.vue'
import Chat from './pages/Chat.vue'
import Login from './pages/Login.vue'
import './style.css'
const routes = [
{ path: '/login', component: Login },
{ path: '/', component: Home, meta: { requiresAuth: true } },
{ path: '/chat/:assistantKey?', component: Chat, props: true, meta: { requiresAuth: true } }
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Authentication guard
router.beforeEach((to, from, next) => {
const currentUser = localStorage.getItem('currentUser')
const requiresAuth = to.meta.requiresAuth
if (requiresAuth && !currentUser) {
next('/login')
} else if (to.path === '/login' && currentUser) {
next('/chat')
} else {
next()
}
})
const app = createApp(App)
app.use(router)
app.mount('#app')

998
admin/src/pages/Chat.vue Normal file
View file

@ -0,0 +1,998 @@
<template>
<div class="chat-page">
<div v-if="loading" class="loading">
Loading agents...
</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<div v-else class="main-layout">
<!-- Chat History Sidebar -->
<div class="chat-history-sidebar">
<div class="sidebar-header">
<h3>Chat History</h3>
<button @click="startNewChat" class="new-chat-btn">
New Chat
</button>
</div>
<!-- LOGOUT BUTTON - ALWAYS VISIBLE -->
<div class="logout-section">
<button @click="logout" class="logout-button" title="Click to Logout">
🚪 LOGOUT
</button>
</div>
<div class="user-info">
<div v-if="currentUser" class="user-display">
<div class="user-avatar">{{ currentUser.role === 'admin' ? '👑' : '👤' }}</div>
<div class="user-details">
<div class="user-name">{{ currentUser.name }}</div>
<div class="user-email">{{ currentUser.email }}</div>
</div>
</div>
<div v-else class="user-display">
<div class="user-avatar">👤</div>
<div class="user-details">
<div class="user-name">User</div>
<div class="user-email">Loading...</div>
</div>
</div>
</div>
<div class="conversations-list">
<div v-if="conversations.length === 0" class="no-conversations">
No conversations yet
</div>
<div
v-for="conversation in conversations"
:key="conversation.id"
:class="['conversation-item', { active: currentConversationId === conversation.id }]"
>
<div class="conversation-content" @click="loadConversation(conversation.id)">
<div class="conversation-preview">
<div class="agent-name">{{ conversation.assistant?.name || 'Unknown Agent' }}</div>
<div class="conversation-title">{{ getConversationTitle(conversation) }}</div>
<div class="conversation-time">{{ formatConversationTime(conversation.lastMessageAt) }}</div>
</div>
</div>
<button
@click.stop="deleteConversation(conversation.id)"
class="delete-conversation-btn"
title="Delete conversation"
>
🗑
</button>
</div>
</div>
</div>
<div class="chat-container">
<!-- Agent Selection Dropdown -->
<div class="agent-selector card mb-4">
<div class="card-body">
<h3>Choose Your Agent</h3>
<select
v-model="selectedAgentKey"
@change="changeAgent"
class="agent-dropdown"
>
<option value="">Select an agent...</option>
<option
v-for="agent in agents"
:key="agent.key"
:value="agent.key"
>
{{ agent.name }} - {{ agent.description }}
</option>
</select>
</div>
</div>
<!-- Chat Messages -->
<div class="chat-messages card">
<div class="card-body">
<div v-if="messages.length === 0 && selectedAgent" class="empty-chat">
<div v-html="formatMessage(getStarterMessage())"></div>
</div>
<div v-else-if="!selectedAgent" class="no-agent">
<p>👆 Please select an agent from the dropdown above to start chatting!</p>
</div>
<div v-else class="messages-container" ref="messagesContainer">
<div
v-for="message in messages"
:key="message.id"
:class="['message', message.role]"
>
<div class="message-content">
<div class="message-text" v-html="formatMessage(message.content)"></div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
</div>
<div v-if="isStreaming" class="message assistant streaming">
<div class="message-content">
<div class="message-text" v-html="formatMessage(streamingMessage)"></div>
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Chat Input -->
<div class="chat-input-section">
<div class="chat-input-container">
<textarea
v-model="newMessage"
@keydown.enter.shift.exact="addNewline"
@keydown.enter.exact="sendMessage"
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
rows="1"
ref="messageInput"
:disabled="isSending"
></textarea>
<button
@click="sendMessage"
:disabled="!newMessage.trim() || isSending"
class="send-button"
>
<span v-if="isSending"></span>
<span v-else>Send 🚀</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { agentsAPI, chatAPI } from '../services/api.js'
import { marked } from 'marked'
export default {
name: 'Chat',
props: ['agentKey', 'assistantKey'], // Keep assistantKey for backward compatibility
data() {
return {
agents: [],
selectedAgent: null,
selectedAgentKey: '',
selectedAssistantKey: '', // Keep for backward compatibility
messages: [],
newMessage: '',
conversationId: null,
currentConversationId: null,
conversations: [],
loading: true,
error: null,
isSending: false,
isStreaming: false,
streamingMessage: '',
userId: null,
currentUser: null,
conversationRefreshInterval: null
}
},
async mounted() {
this.currentUser = this.getCurrentUser()
if (!this.currentUser) {
this.$router.push('/login')
return
}
this.userId = this.getUserId()
if (!this.userId) {
this.$router.push('/login')
return
}
await this.loadAgents()
await this.loadConversations()
// Set up periodic conversation list refresh
this.conversationRefreshInterval = setInterval(() => {
if (this.userId) {
this.loadConversations()
}
}, 60000) // Refresh every 60 seconds
// Refresh conversations when window gains focus
this.onWindowFocus = () => {
if (this.userId) {
this.loadConversations()
}
}
window.addEventListener('focus', this.onWindowFocus)
const key = this.agentKey || this.assistantKey
if (key) {
this.selectedAgentKey = key
this.selectedAssistantKey = key // Keep for backward compatibility
await this.selectAgent(key)
}
},
beforeUnmount() {
if (this.conversationRefreshInterval) {
clearInterval(this.conversationRefreshInterval)
}
window.removeEventListener('focus', this.onWindowFocus)
},
watch: {
agentKey() {
const key = this.agentKey || this.assistantKey
if (key) {
this.selectedAgentKey = key
this.selectedAssistantKey = key
this.selectAgent(key)
}
},
assistantKey() {
const key = this.agentKey || this.assistantKey
if (key) {
this.selectedAgentKey = key
this.selectedAssistantKey = key
this.selectAgent(key)
}
},
messages: {
handler() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
deep: true
},
userId: {
handler(newUserId) {
if (newUserId) {
this.loadConversations()
}
},
immediate: true
}
},
methods: {
getUserId() {
const currentUser = localStorage.getItem('currentUser')
if (!currentUser) {
this.$router.push('/login')
return null
}
const user = JSON.parse(currentUser)
return this.hashUserId(user.email)
},
hashUserId(email) {
// Create a consistent UUID-like string from email
let hash = 0
for (let i = 0; i < email.length; i++) {
hash = ((hash << 5) - hash + email.charCodeAt(i)) & 0xffffffff
}
// Convert to positive number and create UUID-like format
const positiveHash = Math.abs(hash).toString(16).padStart(8, '0')
return `${positiveHash.substr(0, 8)}-${positiveHash.substr(0, 4)}-4${positiveHash.substr(1, 3)}-8${positiveHash.substr(2, 3)}-${positiveHash.padEnd(12, '0').substr(0, 12)}`
},
getCurrentUser() {
const currentUser = localStorage.getItem('currentUser')
return currentUser ? JSON.parse(currentUser) : null
},
logout() {
localStorage.removeItem('currentUser')
this.$router.push('/login')
},
async loadAgents() {
try {
this.loading = true
this.error = null
const data = await agentsAPI.getAll()
this.agents = data.agents || data.assistants // Support both old and new response format
} catch (error) {
this.error = 'Failed to load agents: ' + error.message
console.error('Error loading agents:', error)
} finally {
this.loading = false
}
},
async selectAgent(key) {
try {
this.selectedAgent = await agentsAPI.getByKey(key)
this.clearChat()
} catch (error) {
this.error = 'Failed to load agent: ' + error.message
console.error('Error loading agent:', error)
}
},
async changeAgent() {
if (this.selectedAgentKey) {
await this.selectAgent(this.selectedAgentKey)
this.$router.push(`/chat/${this.selectedAgentKey}`)
}
},
// Keep old methods for backward compatibility
async selectAssistant(key) { await this.selectAgent(key) },
async changeAssistant() { await this.changeAgent() },
getStarterMessage() {
if (!this.selectedAgent?.systemPrompt) return 'Starter Message'
const match = this.selectedAgent.systemPrompt.match(/STARTER MESSAGE:\s*"([^"]+)"/i)
return match ? match[1] : `Hello! I'm ${this.selectedAgent.name}. ${this.selectedAgent.description} How can I help you today?`
},
async sendMessage() {
if (!this.newMessage.trim() || this.isSending || !this.selectedAgent) return
const userMessage = {
id: Date.now(),
role: 'user',
content: this.newMessage,
timestamp: new Date()
}
this.messages.push(userMessage)
const messageToSend = this.newMessage
this.newMessage = ''
this.isSending = true
this.isStreaming = true
this.streamingMessage = ''
try {
await chatAPI.sendStreamingMessage({
messages: [{ role: 'user', content: messageToSend }],
assistantKey: this.selectedAgent.key,
conversationId: this.conversationId,
userId: this.userId
}, (data) => {
if (data.content) {
this.streamingMessage += data.content
}
if (data.done) {
this.messages.push({
id: Date.now() + 1,
role: 'assistant',
content: this.streamingMessage,
timestamp: new Date()
})
this.conversationId = data.conversationId
this.currentConversationId = data.conversationId
this.isStreaming = false
this.streamingMessage = ''
// Refresh conversations list immediately
this.loadConversations()
}
})
} catch (error) {
console.error('Error sending message:', error)
this.messages.push({
id: Date.now() + 1,
role: 'assistant',
content: 'Sorry, I encountered an error. Please try again.',
timestamp: new Date()
})
this.isStreaming = false
this.streamingMessage = ''
} finally {
this.isSending = false
}
},
addNewline() {
this.newMessage += '\n'
},
clearChat() {
this.messages = []
this.conversationId = null
this.streamingMessage = ''
this.isStreaming = false
},
formatMessage(content) {
return marked(content || '', { breaks: true })
},
formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString()
},
scrollToBottom() {
const container = this.$refs.messagesContainer
if (container) {
container.scrollTop = container.scrollHeight
}
},
async loadConversations() {
try {
if (!this.userId) {
console.log('No userId available for loading conversations')
return
}
console.log(`Loading conversations for user: ${this.userId}`)
const data = await chatAPI.getConversations(this.userId)
const newConversations = data.conversations || []
console.log(`Loaded ${newConversations.length} conversations`)
this.conversations = newConversations
} catch (error) {
console.error('Error loading conversations:', error)
// Don't clear conversations on error, keep existing ones
}
},
async loadConversation(conversationId) {
try {
this.currentConversationId = conversationId
this.conversationId = conversationId
const data = await chatAPI.getConversationMessages(conversationId)
this.messages = data.messages.map(msg => ({
id: msg.id,
role: msg.role,
content: msg.content,
timestamp: new Date(msg.createdAt)
}))
// Find the agent for this conversation
const conversation = this.conversations.find(c => c.id === conversationId)
if (conversation && conversation.assistant) {
this.selectedAgent = await agentsAPI.getByKey(conversation.assistant.key)
this.selectedAgentKey = conversation.assistant.key
this.$router.push(`/chat/${conversation.assistant.key}`)
}
} catch (error) {
console.error('Error loading conversation:', error)
}
},
startNewChat() {
this.clearChat()
this.currentConversationId = null
this.$router.push('/chat')
},
getConversationTitle(conversation) {
// Use generated title if available, otherwise fallback to agent-based title
let title = conversation.title
if (!title) {
// For older conversations without generated titles
const agentName = conversation.assistant?.name || 'Unknown Agent'
title = `Chat with ${agentName}`
}
// Truncate long titles
return title.length > 50 ? title.substring(0, 50) + '...' : title
},
formatConversationTime(timestamp) {
if (!timestamp) return ''
const date = new Date(timestamp)
const now = new Date()
const diffMs = now - date
const diffHours = diffMs / (1000 * 60 * 60)
if (diffHours < 24) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
} else if (diffHours < 24 * 7) {
return date.toLocaleDateString([], { weekday: 'short' })
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
}
},
async deleteConversation(conversationId) {
if (!confirm('Are you sure you want to delete this conversation? This action cannot be undone.')) {
return
}
try {
const response = await fetch(`/api/chat/conversations/${conversationId}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error('Failed to delete conversation')
}
// Remove conversation from local list
this.conversations = this.conversations.filter(c => c.id !== conversationId)
// If this was the current conversation, clear chat
if (this.currentConversationId === conversationId) {
this.clearChat()
this.currentConversationId = null
this.$router.push('/chat')
}
} catch (error) {
console.error('Error deleting conversation:', error)
alert('Failed to delete conversation. Please try again.')
}
}
}
}
</script>
<style scoped>
.main-layout {
display: flex;
height: 100vh;
max-width: 1400px;
margin: 0 auto;
}
.chat-history-sidebar {
width: 300px;
background: #f8f9fa;
border-right: 1px solid #e5e7eb;
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h3 {
margin: 0;
color: #1f2937;
font-size: 1.1rem;
font-weight: 600;
}
.new-chat-btn {
padding: 0.5rem 1rem;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
}
.new-chat-btn:hover {
background: #5a67d8;
}
.logout-section {
padding: 1rem;
border-bottom: 2px solid #dc2626;
background: #fef2f2;
}
.logout-button {
width: 100%;
padding: 0.75rem 1rem;
background: #dc2626;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s;
text-align: center;
}
.logout-button:hover {
background: #b91c1c;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
}
.user-info {
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
gap: 0.75rem;
background: #f9fafb;
}
.user-display {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
}
.user-avatar {
width: 2.5rem;
height: 2.5rem;
background: #e0e7ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.user-details {
flex: 1;
min-width: 0;
}
.user-name {
font-weight: 600;
color: #1f2937;
font-size: 0.9rem;
margin-bottom: 0.125rem;
}
.user-email {
color: #6b7280;
font-size: 0.75rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logout-btn {
padding: 0.5rem;
background: none;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
color: #6b7280;
transition: all 0.2s;
}
.logout-btn:hover {
background: #f3f4f6;
color: #dc2626;
}
.conversations-list {
padding: 0.5rem 0;
}
.conversation-item {
display: flex;
align-items: center;
border-bottom: 1px solid #f3f4f6;
transition: background-color 0.2s;
}
.conversation-item:hover {
background: #f3f4f6;
}
.conversation-item.active {
background: #eef2ff;
border-right: 3px solid #667eea;
}
.conversation-content {
flex: 1;
padding: 0.75rem 1rem;
cursor: pointer;
}
.delete-conversation-btn {
padding: 0.5rem;
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: #9ca3af;
border-radius: 4px;
margin-right: 0.5rem;
transition: all 0.2s;
opacity: 0;
}
.conversation-item:hover .delete-conversation-btn {
opacity: 1;
}
.delete-conversation-btn:hover {
background: #fee2e2;
color: #dc2626;
transform: scale(1.1);
}
.conversation-preview .agent-name {
font-weight: 600;
color: #1f2937;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.conversation-preview .conversation-title {
color: #6b7280;
font-size: 0.8rem;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-preview .conversation-time {
color: #9ca3af;
font-size: 0.75rem;
}
.no-conversations {
padding: 2rem 1rem;
text-align: center;
color: #9ca3af;
font-style: italic;
}
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1;
min-width: 0;
}
.agent-selector {
flex-shrink: 0;
}
.agent-selector h3 {
margin-bottom: 1rem;
color: #1f2937;
font-weight: 600;
}
.agent-dropdown {
width: 100%;
padding: 1rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 1rem;
background: white;
cursor: pointer;
}
.agent-dropdown:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Keep old styles for backward compatibility */
.assistant-selector { @extend .agent-selector; }
.assistant-dropdown { @extend .agent-dropdown; }
.assistant-info {
flex: 1;
}
.assistant-header .card-body {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.assistant-name {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.assistant-description {
color: #6b7280;
margin-bottom: 1rem;
}
.assistant-meta {
display: flex;
gap: 0.5rem;
}
.badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: #f3f4f6;
color: #374151;
}
.chat-actions {
display: flex;
gap: 1rem;
}
.chat-messages {
flex: 1;
overflow: hidden;
}
.chat-messages .card-body {
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
}
.empty-chat {
text-align: center;
padding: 2rem;
color: #374151;
background: #f9fafb;
border-radius: 8px;
margin: 1rem;
}
.no-agent {
text-align: center;
padding: 3rem;
color: #6b7280;
font-style: italic;
}
/* Keep old styles for backward compatibility */
.no-assistant { @extend .no-agent; }
.messages-container {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
display: flex;
max-width: 80%;
}
.message.user {
margin-left: auto;
}
.message.user .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.message.assistant {
margin-right: auto;
}
.message.assistant .message-content {
background: #f3f4f6;
color: #1f2937;
}
.message-content {
padding: 1rem;
border-radius: 12px;
position: relative;
}
.message-text {
margin-bottom: 0.5rem;
}
.message-text :deep(p) {
margin: 0.5rem 0;
}
.message-text :deep(pre) {
background: rgba(0,0,0,0.1);
padding: 0.5rem;
border-radius: 4px;
overflow-x: auto;
}
.message-time {
font-size: 0.75rem;
opacity: 0.7;
}
.streaming .message-content {
background: #f9fafb;
border: 2px dashed #e5e7eb;
}
.typing-indicator {
display: flex;
gap: 0.25rem;
margin-top: 0.5rem;
}
.typing-indicator span {
width: 6px;
height: 6px;
border-radius: 50%;
background: #9ca3af;
animation: typing 1.5s ease-in-out infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-10px);
}
}
.chat-input-section {
flex-shrink: 0;
padding: 1rem 0;
}
.chat-input-container {
display: flex;
gap: 1rem;
align-items: end;
}
.chat-input-container textarea {
flex: 1;
padding: 1rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 1rem;
resize: vertical;
min-height: 50px;
max-height: 150px;
}
.chat-input-container textarea:focus {
outline: none;
border-color: #667eea;
}
.send-button {
padding: 1rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.send-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.send-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

222
admin/src/pages/Home.vue Normal file
View file

@ -0,0 +1,222 @@
<template>
<div class="home-page">
<div class="hero-section text-center mb-8">
<h1 class="hero-title">Ideas Generator 2025</h1>
<p class="hero-subtitle">Powered by OpenAI's Responses API with 48 Specialized AI Assistants</p>
</div>
<div v-if="loading" class="loading">
Loading assistants...
</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<div v-else class="assistants-section">
<h2 class="section-title mb-6">Choose Your AI Assistant</h2>
<div class="categories" v-if="groupedAssistants">
<div
v-for="(assistants, category) in groupedAssistants"
:key="category"
class="category-section mb-8"
>
<h3 class="category-title">{{ formatCategoryName(category) }}</h3>
<div class="grid grid-3">
<div
v-for="assistant in assistants"
:key="assistant.key"
class="assistant-card card"
>
<div class="card-body">
<h4 class="assistant-name">{{ assistant.name }}</h4>
<p class="assistant-description">{{ assistant.description }}</p>
<div class="assistant-meta">
<span class="model-badge">{{ assistant.model }}</span>
<span class="temp-badge">Temp: {{ assistant.temperature }}</span>
</div>
<router-link
:to="`/chat/${assistant.key}`"
class="btn btn-primary start-chat-btn"
>
Start Chat 💬
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="stats-section mt-8">
<div class="card">
<div class="card-body text-center">
<h3>Migration Complete! 🎉</h3>
<p class="mb-4">Successfully migrated from Make.com workflow to Node.js backend</p>
<div class="stats-grid">
<div class="stat">
<div class="stat-number">{{ totalAssistants }}</div>
<div class="stat-label">AI Assistants</div>
</div>
<div class="stat">
<div class="stat-number"></div>
<div class="stat-label">Responses API</div>
</div>
<div class="stat">
<div class="stat-number">🚀</div>
<div class="stat-label">Ready to Use</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { assistantsAPI } from '../services/api.js'
export default {
name: 'Home',
data() {
return {
assistants: [],
groupedAssistants: {},
totalAssistants: 0,
loading: true,
error: null
}
},
async mounted() {
await this.loadAssistants()
},
methods: {
async loadAssistants() {
try {
this.loading = true
const data = await assistantsAPI.getAll()
this.assistants = data.assistants
this.groupedAssistants = data.groupedByCategory
this.totalAssistants = data.total
} catch (error) {
this.error = 'Failed to load assistants: ' + error.message
console.error('Error loading assistants:', error)
} finally {
this.loading = false
}
},
formatCategoryName(category) {
return category.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}
}
}
</script>
<style scoped>
.hero-title {
font-size: 3rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 1rem;
}
.hero-subtitle {
font-size: 1.2rem;
color: #6b7280;
max-width: 600px;
margin: 0 auto;
}
.section-title {
font-size: 2rem;
font-weight: 700;
color: #1f2937;
text-align: center;
}
.category-title {
font-size: 1.5rem;
font-weight: 600;
color: #374151;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e5e7eb;
}
.assistant-card {
transition: transform 0.2s, box-shadow 0.2s;
height: fit-content;
}
.assistant-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.assistant-name {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 0.5rem;
}
.assistant-description {
color: #6b7280;
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.assistant-meta {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.model-badge, .temp-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: #f3f4f6;
color: #374151;
}
.start-chat-btn {
width: 100%;
justify-content: center;
}
.stats-section h3 {
color: #1f2937;
margin-bottom: 0.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin-top: 2rem;
}
.stat {
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: #667eea;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
}
</style>

167
admin/src/pages/Login.vue Normal file
View file

@ -0,0 +1,167 @@
<template>
<div class="login-page">
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1>Ideas Generator 2025</h1>
<p>Choose your account to continue</p>
</div>
<div class="login-form">
<div class="user-options">
<button
@click="loginAs('admin@oliver.agency')"
class="user-button admin-button"
>
<div class="user-icon">👑</div>
<div class="user-info">
<div class="user-name">Administrator</div>
<div class="user-email">admin@oliver.agency</div>
</div>
</button>
<button
@click="loginAs('user@oliver.agency')"
class="user-button user-button"
>
<div class="user-icon">👤</div>
<div class="user-info">
<div class="user-name">User</div>
<div class="user-email">user@oliver.agency</div>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
methods: {
loginAs(email) {
// Store user info in localStorage
const userInfo = {
email,
name: email === 'admin@oliver.agency' ? 'Administrator' : 'User',
role: email === 'admin@oliver.agency' ? 'admin' : 'user',
loginTime: new Date().toISOString()
}
localStorage.setItem('currentUser', JSON.stringify(userInfo))
// Redirect to chat page
this.$router.push('/chat')
}
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}
.login-container {
width: 100%;
max-width: 400px;
}
.login-card {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: #1f2937;
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.login-header p {
color: #6b7280;
font-size: 1rem;
margin: 0;
}
.user-options {
display: flex;
flex-direction: column;
gap: 1rem;
}
.user-button {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
background: white;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.user-button:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
transform: translateY(-1px);
}
.admin-button:hover {
border-color: #f59e0b;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
}
.user-icon {
font-size: 2rem;
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
border-radius: 50%;
}
.user-info {
text-align: left;
flex: 1;
}
.user-name {
font-weight: 600;
color: #1f2937;
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.user-email {
color: #6b7280;
font-size: 0.9rem;
}
.admin-button .user-icon {
background: #fef3c7;
}
.user-button .user-icon {
background: #e0e7ff;
}
</style>

94
admin/src/services/api.js Normal file
View file

@ -0,0 +1,94 @@
import axios from 'axios'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
})
api.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Error:', error.response?.data || error.message)
return Promise.reject(error)
}
)
export const agentsAPI = {
async getAll() {
const response = await api.get('/assistants')
return response.data
},
async getByKey(key) {
const response = await api.get(`/assistants/${key}`)
return response.data
}
}
// Keep old name for backward compatibility during migration
export const assistantsAPI = agentsAPI
export const chatAPI = {
async sendMessage(data) {
const response = await api.post('/chat/completions', data)
return response.data
},
async sendStreamingMessage(data, onChunk) {
const response = await fetch(`${API_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...data, stream: true })
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6))
if (onChunk) onChunk(data)
} catch (e) {
console.warn('Failed to parse SSE data:', line)
}
}
}
}
} finally {
reader.releaseLock()
}
},
async getConversationMessages(conversationId, limit = 50, offset = 0) {
const response = await api.get(`/chat/conversations/${conversationId}/messages`, {
params: { limit, offset }
})
return response.data
},
async getConversations(userId, limit = 20, offset = 0) {
const response = await api.get('/chat/conversations', {
params: { userId, limit, offset }
})
return response.data
}
}
export default api

135
admin/src/style.css Normal file
View file

@ -0,0 +1,135 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8fafc;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
overflow: hidden;
}
.card-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
}
.card-body {
padding: 1.5rem;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.btn-outline {
background: transparent;
color: #667eea;
border: 2px solid #667eea;
}
.btn-outline:hover {
background: #667eea;
color: white;
}
.grid {
display: grid;
gap: 1.5rem;
}
.grid-2 {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.grid-3 {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.text-center {
text-align: center;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: #6b7280;
}
.error {
background: #fef2f2;
color: #dc2626;
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #dc2626;
margin: 1rem 0;
}
.success {
background: #f0fdf4;
color: #16a34a;
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #16a34a;
margin: 1rem 0;
}

15
admin/vite.config.js Normal file
View file

@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}
})

View file

@ -9,8 +9,8 @@ DATABASE_PASS=
REDIS_URL=redis://localhost:6379
# OpenAI Configuration (REQUIRED - Replace with your actual keys)
OPENAI_API_KEY=sk-your-actual-openai-key-here
OPENAI_ORG_ID=your-org-id-here
OPENAI_API_KEY=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
OPENAI_ORG_ID=org-HSioKMud1tZBdpWhBjJE6SLe
# Server Configuration
PORT=3000
@ -20,7 +20,8 @@ NODE_ENV=development
SKIP_AUTH=true
ENABLE_CORS=true
LOG_LEVEL=debug
SKIP_RATE_LIMITING=true
# Optional Features
ENABLE_TITLE_GENERATION=true
ENABLE_MODERATION=true
ENABLE_MODERATION=true

48
server/.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Logs
logs
*.log
# Operating System generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
# Build outputs
dist/
build/
# Temporary files
*.tmp
*.temp

25
server/config/database.js Normal file
View file

@ -0,0 +1,25 @@
const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize(process.env.DATABASE_URL, {
dialect: 'postgres',
logging: process.env.NODE_ENV === 'development' ? console.log : false,
pool: {
max: 10,
min: 0,
acquire: 30000,
idle: 10000,
},
});
const testConnection = async () => {
try {
await sequelize.authenticate();
console.log('✅ Database connection established successfully.');
} catch (error) {
console.error('❌ Unable to connect to the database:', error);
process.exit(1);
}
};
module.exports = { sequelize, testConnection };

View file

@ -4,6 +4,12 @@ const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();
const { testConnection } = require('./config/database');
const { generalLimiter } = require('./middleware/rateLimiter');
const errorHandler = require('./middleware/errorHandler');
const chatRouter = require('./routes/chat');
const assistantsRouter = require('./routes/assistants');
const app = express();
const PORT = process.env.PORT || 3000;
@ -23,6 +29,9 @@ app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Rate limiting
app.use(generalLimiter);
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
@ -34,30 +43,28 @@ app.get('/health', (req, res) => {
});
});
// API routes placeholder
app.use('/api', (req, res) => {
// API routes
app.use('/api/chat', chatRouter);
app.use('/api/assistants', assistantsRouter);
// API info endpoint
app.get('/api', (req, res) => {
res.json({
message: 'Ideas Generator 2025 API',
version: '1.0.0',
status: 'Under Development',
status: 'Active',
endpoints: {
health: '/health',
api: '/api',
chat: '/api/chat (coming soon)',
assistants: '/api/assistants (coming soon)',
conversations: '/api/conversations (coming soon)'
chat: '/api/chat/completions',
assistants: '/api/assistants',
conversations: '/api/chat/conversations/:id/messages'
}
});
});
// Basic error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong!'
});
});
// Error handling middleware
app.use(errorHandler);
// 404 handler
app.use((req, res) => {
@ -69,13 +76,18 @@ app.use((req, res) => {
});
// Start server
app.listen(PORT, () => {
app.listen(PORT, async () => {
console.log(`🚀 Ideas Gen 2025 Server running on port ${PORT}`);
console.log(`🏥 Health check: http://localhost:${PORT}/health`);
console.log(`🔧 API endpoint: http://localhost:${PORT}/api`);
console.log(`💬 Chat endpoint: http://localhost:${PORT}/api/chat/completions`);
console.log(`🤖 Assistants endpoint: http://localhost:${PORT}/api/assistants`);
console.log(`📊 Environment: ${process.env.NODE_ENV}`);
console.log(`📁 Database: ${process.env.DATABASE_NAME || 'Not configured'}`);
// Test database connection
await testConnection();
// Log important environment status
if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY.includes('your-actual')) {
console.warn('⚠️ WARNING: OpenAI API key not configured! Update .env file.');

View file

@ -0,0 +1,55 @@
const errorHandler = (err, req, res, next) => {
console.error('Error occurred:', {
message: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
path: req.path,
method: req.method,
timestamp: new Date().toISOString(),
});
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation Error',
message: err.message,
details: err.details || {},
});
}
if (err.name === 'SequelizeValidationError') {
return res.status(400).json({
error: 'Database Validation Error',
message: 'Invalid data provided',
details: err.errors?.map(e => ({ field: e.path, message: e.message })) || [],
});
}
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
error: 'Conflict',
message: 'Resource already exists',
details: err.errors?.map(e => ({ field: e.path, message: e.message })) || [],
});
}
if (err.status || err.statusCode) {
return res.status(err.status || err.statusCode).json({
error: err.name || 'Error',
message: err.message,
});
}
if (err.code === 'OPENAI_API_ERROR') {
return res.status(502).json({
error: 'OpenAI Service Error',
message: 'There was an issue communicating with OpenAI. Please try again.',
});
}
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong!',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
module.exports = errorHandler;

View file

@ -0,0 +1,35 @@
const rateLimit = require('express-rate-limit');
const createRateLimiter = (windowMs, max, message) => {
return rateLimit({
windowMs,
max,
message: {
error: 'Too Many Requests',
message,
retryAfter: Math.ceil(windowMs / 1000),
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return process.env.SKIP_RATE_LIMITING === 'true';
},
});
};
const chatLimiter = createRateLimiter(
15 * 60 * 1000, // 15 minutes
20, // limit each IP to 20 requests per windowMs
'Too many chat requests from this IP, please try again later.'
);
const generalLimiter = createRateLimiter(
15 * 60 * 1000, // 15 minutes
1000, // limit each IP to 1000 requests per windowMs (high limit for development)
'Too many requests from this IP, please try again later.'
);
module.exports = {
chatLimiter,
generalLimiter,
};

View file

@ -0,0 +1,28 @@
require('dotenv').config();
const { sequelize } = require('../config/database');
const { testConnection } = require('../config/database');
require('../models');
const migrate = async () => {
try {
console.log('🔄 Starting database migration...');
await testConnection();
await sequelize.sync({ force: false, alter: true });
console.log('✅ Database migration completed successfully!');
console.log('📊 All models synchronized with database');
process.exit(0);
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
}
};
if (require.main === module) {
migrate();
}
module.exports = migrate;

651
server/migrations/seed.js Normal file
View file

@ -0,0 +1,651 @@
require('dotenv').config();
const { Assistant } = require('../models/index');
const agentsData = [
{
key: 'smart-goals',
name: 'Smart Goals',
description: 'You are a SMART Goal Conversion Assistant, designed to help users transform their general goals and objectives into well...',
category: 'smart-goals',
systemPrompt: `ROLE AND PURPOSE:
You are a SMART Goal Conversion Assistant, designed to help users transform their general goals and objectives into well-structured SMART goals (Specific, Measurable, Achievable, Relevant, Time-bound).
PRIMARY FUNCTIONS:
1. Goal Analysis
- Listen to or read the user's initial goal
- Identify missing SMART components
- Ask clarifying questions to gather necessary information
2. SMART Framework Application
Break down each component:
- Specific: Help define exactly what needs to be accomplished
- Measurable: Establish concrete criteria for measuring progress
- Achievable: Ensure the goal is realistic and attainable
- Relevant: Confirm the goal aligns with broader objectives
- Time-bound: Set specific deadlines and milestones
3. Interactive Guidance
- Ask probing questions for each SMART component
- Provide examples and suggestions
- Help users refine vague elements
OPERATIONAL GUIDELINES:
1. When receiving a goal, first acknowledge it and then:
- Analyze which SMART elements are present/missing
- Start with open-ended questions about missing elements
- Guide users through each component systematically
2. Use this question framework:
- Specific: "What exactly do you want to accomplish?"
- Measurable: "How will you measure success?"
- Achievable: "What resources/skills do you need?"
- Relevant: "Why is this goal important to you/your organization?"
- Time-bound: "When do you want to achieve this by?"
3. Provide reformulation:
- After gathering information, present the reformulated SMART goal
- Explain how each component has been addressed
- Seek confirmation and refinement from the user
RESPONSE STRUCTURE:
1. Initial Response:
- Acknowledge the original goal
- Identify which SMART elements need development
- Ask first clarifying question
2. Follow-up Responses:
- Address one SMART component at a time
- Provide specific examples related to the user's context
- Offer suggestions for improvement
3. Final Output:
- Present the complete SMART goal
- Break down how each component is addressed
- Offer to make any final adjustments
TONE AND STYLE:
- Maintain a helpful, encouraging tone
- Be patient and supportive
- Use clear, concise language
- Avoid technical jargon unless necessary
EXAMPLE INTERACTION:
User: "I want to increase our company's sales."
Assistant's Response:
"Thank you for sharing your goal about increasing sales. Let's make this SMART:
To make this more Specific:
- Which products/services do you want to increase sales for?
- In which market or region?
Could you tell me more about exactly what sales increase you're targeting?"
[Continue with similar prompts for each SMART component]
ERROR HANDLING:
- If users provide vague responses, ask for clarification
- If goals seem unrealistic, tactfully suggest adjustments
- If users struggle with any component, provide relevant examples
LIMITATIONS:
- Acknowledge when goals might need professional input
- Flag when goals might not be realistic
- Recommend seeking expert advice when appropriate
CUSTOMIZATION:
- Adapt language and examples to the user's industry/context
- Scale complexity based on user's familiarity with SMART goals
- Provide industry-specific metrics when relevant
END GOAL:
The assistant should help users transform any goal into a clear, actionable SMART goal that provides a concrete framework for achievement and progress tracking.
STARTER MESSAGE: "Hello! I am Smart Goals. I can help you with smart goals strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_HhVXiWRswCDqFASEDFMRxo0V' },
isActive: true,
sortOrder: 1,
},
{
key: 'creator-bot-push-the-boundaries-of-technology',
name: 'Push The Boundaries Of Technology',
description: 'Be an innovator and move your industry forward.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Be an innovator and move your industry forward.
Aim to create a new product, service or way to advertise that extends the value of your brand.
Come up with something patentable, something that just wouldnt have been possible a few years ago and is only achievable now thanks to the advances in technology and your keen ability to press them into your service.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Push The Boundaries Of Technology. I can help you with push the boundaries of technology strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_xnFLPlogjQX3Kbac34fBlz80' },
isActive: true,
sortOrder: 2,
},
{
key: 'creator-bot-dress-up-as-news-or-entertainment',
name: 'Dress Up As News Or Entertainment',
description: 'The truth is that people dont like ads.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
The truth is that people dont like ads.
So try and get under their ad-radar by making your ad look as little like an ad as possible.
Package it as a home video, a documentary film, a music video, a gif, a television program, a magazine article, a news report or a Facebook post.
Its sly, for sure. One could even argue that its evil.
But if you do it subtly, your audience wont resent having been tricked into spending time with a commercial message.
And if its truly entertaining, funny or informative, they might even share it with their friends. You never know.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Dress Up As News Or Entertainment. I can help you with dress up as news or entertainment strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_QRW0OZxkiwPMBXILYdaDSxd2' },
isActive: true,
sortOrder: 3,
},
{
key: 'creator-bot-replace-a-real-experience-with-a-virtual-one',
name: 'Replace A Real Experience With A Virtual One',
description: 'We now live in a time when its possible to create any experience through speakers and screens.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
We now live in a time when its possible to create any experience through speakers and screens.
Think of places you can now travel to with the help of digital technology that you couldnt go before.
Think of the things you can do now that could only be done in the past by the fortunate, the wealthy or the physically fit.
You have the power to transport your audience into the past, into the future, into outer space, across the oceans, to the bottom of the sea, into make-believe land, into each others loving arms or even inside the cluttered, conflicted head of the President of the USA.
All you have to do is figure out your destination. And make the trip emotional.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Replace A Real Experience With A Virtual One. I can help you with replace a real experience with a virtual one strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_TMJau5y7DSmeNwrjclTN6Y6x' },
isActive: true,
sortOrder: 4,
},
{
key: 'creator-bot-find-a-fitting-location',
name: 'Find A Fitting Location',
description: 'Not so long ago, when ad people talked about media, you could be pretty sure they were referring to only print, radio,...',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Not so long ago, when ad people talked about media, you could be pretty sure they were referring to only print, radio, billboards or television.
Thats not true anymore.
Thanks to the internet, social media and advancements in digital technology, any surface at any location can now be used to send a message.
Just by picking the right location to deliver your message, you can be topical, relevant and interesting.
You can be in the exact spot where you appear the most dramatic, competitive and brilliant.
You can be invisible when youre not needed and visible only when you are.
You can be right in peoples faces or deep inside their pockets.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Find A Fitting Location. I can help you with find a fitting location strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_E1To4mnvKv1sM325BO4mx2MH' },
isActive: true,
sortOrder: 5,
},
{
key: 'creator-bot-conduct-a-product-trial',
name: 'Conduct A Product Trial',
description: 'Free trials have been around since the beginning of business.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Free trials have been around since the beginning of business.
Indeed, everyone knows that for a new product, free trials are a good way to recruit customers.
But what if theres nothing new about your product?
Just get people who are not part of your target audience to try it for free.
For instance, if youre selling tea, offer it to people who only drink coffee.
If youre selling a truck, let sports car drivers take a test drive.
If youre marketing a resort, offer a free holiday to people who have never taken one.
Of course, you cant expect to make instant converts of the new group.
But the resulting film might just be entertaining enough to create buzz on social media.
And this fresh look at a familiar experience will reassure your core consumers that your product is still a treat.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Conduct A Product Trial. I can help you with conduct a product trial strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_Yvb1vK5pCpI8JaO9AonQiDCR' },
isActive: true,
sortOrder: 6,
},
{
key: 'creator-bot-partner-with-another-brand',
name: 'Partner With Another Brand',
description: 'Think of other products, services or people that you could tie in with.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Think of other products, services or people that you could tie in with.
Tie-ins not only save on costs, but they also give all the brands involved more eyeballs than they would get on their own.
The trick to a successful tie-in is to find things that go together like peanut butter and jelly.
A coffee brand could tie-in with a music store or a bookstore.
A computer hardware brand could tie-in with a software brand.
A luxury car brand could tie in with a brand that sells premium luggage or golf clubs.
A real-estate company could tie-in with a storage company.
Both brands need to have the same goals, the same audience and preferably the same method of distribution, so everybody has a sweet time and gets to the podium.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Partner With Another Brand. I can help you with partner with another brand strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_jbU6KGnXYGK0CjrF3IfE6CIN' },
isActive: true,
sortOrder: 7,
},
{
key: 'creator-bot-offer-something-irresistible',
name: 'Offer Something Irresistible',
description: 'Come up with an offer your audience just cant turn down.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Come up with an offer your audience just cant turn down.
Were talking about a one-time deal.
But not a sale or a promotional price off on your product.
Instead, an offer that will raise eyebrows, bring the journalists to your door, set the social networks abuzz, go down in history and perhaps even set a new world record.
Theres only one watch out: it has to be relevant to what youre selling.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Offer Something Irresistible. I can help you with offer something irresistible strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_nPLcevnQvt5zIhAB6FhBg8Vj' },
isActive: true,
sortOrder: 8,
},
{
key: 'creator-bot-turn-it-into-a-game',
name: 'Turn It Into A Game',
description: 'Turn your project into something fun, engaging and rewarding: a game.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Turn your project into something fun, engaging and rewarding: a game.
Anything can be gamified.
Gamification, in essence, is about giving people a target to work towards and rewarding their efforts as they progress.
Games tap into many of our natural instinctsour optimism, our desire to do something extraordinary, our willingness to collaborate with others, our resilience when we fail and the satisfaction we get from going past a finish line.
Games can also be a way to change behavior, to discourage bad habits or encourage positive ones.
And games dont necessarily have to be competitive.
In fact, studies show that collaborative games have more appeal than competitive ones.
Nor do games have to be based on fantasy.
Reality-based, non-fiction games can also be attractive.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Turn It Into A Game. I can help you with turn it into a game strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_VwR28kgW0nY74V7tN3IXD5tD' },
isActive: true,
sortOrder: 9,
},
{
key: 'creator-bot-set-up-an-installation',
name: 'Set Up An Installation',
description: 'There are two types of installations.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
There are two types of installations.
The first is a sculpture, ideally three-dimensional, that is passively watched and wondered at.
The other is an interactive set up (using digital, video, sound and physical material) that is touched and played with.
Either way, the installation should have the power to stop people and keep them engaged until your message gets through.
But what should your installation be about?
Start by thinking of ways to use the latest technology to deliver a new experience of the brands benefit.
Remember though that the physical set up is only half the story.
The real power of an installation is in the video that follows, the video that will tell the story of its creation, its set up and its effect on passers-by.
Get that story right and your installation will be able to fly to millions of screens across the world.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Set Up An Installation. I can help you with set up an installation strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_ClyNP4IpnvVRc8MbfycQfy3V' },
isActive: true,
sortOrder: 10,
},
{
key: 'creator-bot-play-a-prank',
name: 'Play A Prank',
description: 'Get ready for some street-theatre.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Get ready for some street-theatre.
Youre going to subject a few unsuspecting people to a wild practical joke in order to highlight a key selling point of your product, then make an entertaining video from the footage that you hope will be shared online.
Prank films are entertaining because they pack action, drama, suspense and laughter in a single event.
They can give a brand an aura of being rather anti-authoritarian and revolutionary.
Not only do they cost less than high-end television commercials, they are also more authentic and down-to-earth.
But staging a successful prank is easier said than done.
You need great timing and emotional intuition.
Its only a good prank if there are laughs at the end, especially from the people at the receiving end.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Play A Prank. I can help you with play a prank strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_bMUlxg4qvANTrnHETooID8NP' },
isActive: true,
sortOrder: 11,
},
{
key: 'creator-bot-conduct-an-experiment',
name: 'Conduct An Experiment',
description: 'Experiments, especially social ones, are now popular for the same reason reality television became popular.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Experiments, especially social ones, are now popular for the same reason reality television became popular.
They appeal to our voyeuristic instincts.
We get to compare ourselves with people who are dumped into situations that we may either wish we could be in, or are relieved that we are not.
For advertisers, they are a great option, because they are cheaper to stage and orchestrate than TV ads.
But in order for your audience to believe your experiments conclusions, it must be unbiased.
And so when you conduct one, you have to follow some scientific principles.
Your experiment should start with the framing of a question or a hypothesis that you want to test.
Moreover, every participant must go through the same procedure.
That doesnt mean that you have to be serious, formal and dull.
Your approach can as lighthearted and entertaining as your subject and your brands tone of voice will allow.
You can also take some liberties in the telling of the story.
You may disclose to your viewers that its a branded experiment right up front. Or you may save it for a reveal right at the end.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Conduct An Experiment. I can help you with conduct an experiment strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_QBbtkcPGCJQrkKgTk7V66FdR' },
isActive: true,
sortOrder: 12,
},
{
key: 'creator-bot-invite-participation',
name: 'Invite Participation',
description: 'When your viewers play an active role in your ad, they are more likely to help spread the message.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
When your viewers play an active role in your ad, they are more likely to help spread the message.
However, getting consumers to participate in advertising is easier said than done.
One way to motivate them is to ask for their opinion.
Granted youll hear some views you didnt want to hear, but thats part of the deal.
A second way is to ask people to be creative.
A third is to get them to contribute towards building something together, perhaps an inspiring project that is so big that it calls for talent coming together from many parts of the globe.
And the final option is to create a platform that enables one group of perhaps privileged people to aid another not-so privileged group.
But no matter how you choose to go about it, you still have to think about how the participants are rewarded.
Remember, the reward doesnt always have to be material.
It can be an emotional reward, say, the satisfaction of helping another human being.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Invite Participation. I can help you with invite participation strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_t6HxiM2VhAMFXLz5s6oQIj2j' },
isActive: true,
sortOrder: 13,
},
{
key: 'creator-bot-crash-someone-elses-party',
name: 'Crash Someone ElseS Party',
description: 'Lets be honest. Your audience has better things to do than watch your ad.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Lets be honest. Your audience has better things to do than watch your ad.
After all, theres a world of movies, TV, gossip and news out there.
So identify other subjects related to yours that are currently trending on social media, pick the one with the most interesting connection to yours and hijack the discussion.
If you do it intelligently, tastefully and with consideration, you will get not just peoples attention but also their appreciation.
The trick is in respecting your consumer.
And that means making them feel rewarded, not cheated.
As Bob Thacker (Senior Marketing Officer at OfficeMax) once said, All advertising is unwanted. So if youre going to crash the party, bring some champagne with you.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Crash Someone ElseS Party. I can help you with crash someone elses party strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_17kunaONMFQhH8Z1Gbqdtuyx' },
isActive: true,
sortOrder: 14,
},
{
key: 'creator-bot-customize-and-personalize',
name: 'Customize And Personalize',
description: 'No one likes to be made to feel like a number.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
No one likes to be made to feel like a number.
Nobody wants to be treated as just another face in a crowd.
But until now, large companies couldnt help but treat their customers that way.
Today, thanks to advances in technology, they can make every member of their audience feel as if their brand exists exclusively for them.
So think of a way to tailor your product, your message or your experience for every individual who views it.
The more customized the experience, the more flattering it is.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Customize And Personalize. I can help you with customize and personalize strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_2yp6BTEEOr1FgTYcalsmU0N7' },
isActive: true,
sortOrder: 15,
},
{
key: 'creator-bot-invent-a-complementary-product',
name: 'Invent A Complementary Product',
description: 'Think of a new product that would perfectly complement your existing one while adding value to the brand.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Think of a new product that would perfectly complement your existing one while adding value to the brand.
It could be a smart social-device that is able to share information with other products or connect customers with each other.
Launching a complimentary product will make the brand newsworthy again.
When you advertise the new product, you naturally advertise the brand.
It also establishes credibility in the industry and attracts customers to the website.
If a full investment in a new product is too daunting, consider launching it with just a limited quantity in a small test market.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Invent A Complementary Product. I can help you with invent a complementary product strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_moKXbLNCRJ3o2aNveIwNzPc5' },
isActive: true,
sortOrder: 16,
},
{
key: 'creator-bot-be-brutally-simple',
name: 'Be Brutally Simple',
description: 'Try to come up with the simplest expression of your proposition.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Try to come up with the simplest expression of your proposition.
Impose restrictions on yourself and eliminate everything thats unnecessary.
Ask yourself how you would cope if you had to execute your idea with very little money.
What if you could use only a single locked-off camera, just one actor, or just one location?
What if you could use no words at all?
What if you werent allowed to use images and had to convey your message in just words?
What if you had only 10 seconds or less to say your piece?
Its often the case that the greater the limitations, the more distilled the idea.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Be Brutally Simple. I can help you with be brutally simple strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_axwu9GVSO6Or4vDB3LlGpbbc' },
isActive: true,
sortOrder: 17,
},
{
key: 'creator-bot-use-the-power-of-cute',
name: 'Use The Power Of Cute',
description: 'If youre ever patted a puppy, watched a cat video on Facebook, or cooed over someones baby, then you are already famil...',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
If youre ever patted a puppy, watched a cat video on Facebook, or cooed over someones baby, then you are already familiar with the power of cute.
Cute not only has the power to melt hearts, it can get people to endure hardship and expense.
Ask any parent who has woken up in the middle of the night to change a nappy or comfort a colicky infant.
Baby animals have just the right features to inspire us to care for them big heads, large eyes, little noses and puffed cheeks.
This is why WWF uses a panda as their logo, and not, for instance, the endangered Chinese giant salamander.
This is why Hello Kitty and Mickey Mouse rake in billions of dollars.
And this is why baby-face cars like the Mini, the Beetle and the Fiat 500 are so popular.
In Japan, cuteness (kawaii) is everywhere, and Japanese businesses, big and small, use cute to sell their products.
Even the Japanese police market themselves with a cute mascot.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Use The Power Of Cute. I can help you with use the power of cute strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_R86GnlXA4sUoPrq7iDEuGFCu' },
isActive: true,
sortOrder: 18,
},
{
key: 'creator-bot-stage-a-spectacle',
name: 'Stage A Spectacle',
description: 'There are two reasons to go down this route.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
There are two reasons to go down this route.
The first is to get on the news and create some buzz around your brand.
The second is to generate content that will get passed around on social media.
Your spectacle could be in the form of a public event, a roadshow, a PR stunt or a film, any of which deliver a payoff that is consistent with your brand promise.
Be ambitious with your spectacle.
If possible, aim to set a world record.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Stage A Spectacle. I can help you with stage a spectacle strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_T3Z17rjnpURjI3x27VDWJBHf' },
isActive: true,
sortOrder: 19,
},
{
key: 'creator-bot-apply-social-pressure',
name: 'Apply Social Pressure',
description: 'Being social creatures, we are driven by and obsessed with what other people think of us.',
category: 'creative',
systemPrompt: `While answering the users questions you will always be Using this technique:
Being social creatures, we are driven by and obsessed with what other people think of us.
We yearn to be liked, admired and respected by our peer group.
And the truth is that we will do almost anything to fit in.
Social pressure, therefore, is a powerful tool that can be used either to reinforce positive behavior (like volunteering with a charity) or to correct negative behavior (like quitting smoking).
The pressure you apply can also be encouraging (a pat on the back and a Well done, you are awesome) or stigmatizing (Hey a**hole, whats wrong with you?).
With an encouraging approach, aim to create a new peer group of people with whom your target group can identify, a group that will help them fit in and hold them accountable for their actions.
Think of peer coaches, real-life buddy support systems and social-media support groups.
Think of social media campaigns that use Facebook and Twitter, like the ones that encourage people to save the rainforest or donate their clothes for earthquake victims.
Alcoholics Anonymous is an example of an encouraging peer support group that aims to correct a negative behavior.
If, however, you choose the stigmatizing approach, make sure that the stigma you create is targeted at the behavior and not the person behind it.
Secondly, ensure that what you are trying to change is indeed a voluntary behavior and not the result of some medical condition that the person has no control over.
and when you do this give more platform ideas as opposed to executional ideas. but always say at the base of your response when they are "these are more platform ideas, if you want executional ones let me know and I'll make them". And in future responses if they ask that make them more executional
STARTER MESSAGE: "Hello! I am Apply Social Pressure. I can help you with apply social pressure strategies and techniques. What would you like to work on today?"`,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
tools: [],
metadata: { originalId: 'asst_itXyVBJEQHmNfJ8BXKNOZh8X' },
isActive: true,
sortOrder: 20,
}
];
const seed = async () => {
try {
console.log('🌱 Starting database seeding...');
// Clear existing agents
await Assistant.destroy({ where: {} });
console.log('🗑️ Cleared existing agents');
for (const agentData of agentsData) {
const agent = await Assistant.create(agentData);
console.log(`✅ Created agent: ${agent.name}`);
}
console.log('✅ Database seeding completed successfully!');
process.exit(0);
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
};
if (require.main === module) {
seed();
}
module.exports = seed;

View file

@ -0,0 +1,83 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Assistant = sequelize.define('Assistant', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
key: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.TEXT,
allowNull: false,
},
category: {
type: DataTypes.ENUM(
'smart-goals',
'business',
'creative',
'personal',
'technical',
'educational',
'health',
'lifestyle'
),
allowNull: false,
},
systemPrompt: {
type: DataTypes.TEXT,
allowNull: false,
},
model: {
type: DataTypes.STRING,
defaultValue: 'gpt-4o',
},
temperature: {
type: DataTypes.FLOAT,
defaultValue: 0.7,
validate: {
min: 0,
max: 2,
},
},
maxTokens: {
type: DataTypes.INTEGER,
defaultValue: 4000,
},
tools: {
type: DataTypes.JSONB,
defaultValue: [],
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {},
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
sortOrder: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
}, {
tableName: 'assistants',
timestamps: true,
indexes: [
{ fields: ['key'], unique: true },
{ fields: ['category'] },
{ fields: ['isActive'] },
{ fields: ['sortOrder'] },
],
});
module.exports = Assistant;

View file

@ -0,0 +1,54 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Conversation = sequelize.define('Conversation', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id',
},
},
assistantId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'assistants',
key: 'id',
},
},
title: {
type: DataTypes.STRING,
allowNull: true,
},
status: {
type: DataTypes.ENUM('active', 'archived', 'deleted'),
defaultValue: 'active',
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {},
},
lastMessageAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
}, {
tableName: 'conversations',
timestamps: true,
indexes: [
{ fields: ['userId'] },
{ fields: ['assistantId'] },
{ fields: ['status'] },
{ fields: ['lastMessageAt'] },
{ fields: ['userId', 'status'] },
],
});
module.exports = Conversation;

53
server/models/Message.js Normal file
View file

@ -0,0 +1,53 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Message = sequelize.define('Message', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
conversationId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'conversations',
key: 'id',
},
},
role: {
type: DataTypes.ENUM('user', 'assistant', 'system'),
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {},
},
tokenCount: {
type: DataTypes.INTEGER,
allowNull: true,
},
model: {
type: DataTypes.STRING,
allowNull: true,
},
finishReason: {
type: DataTypes.STRING,
allowNull: true,
},
}, {
tableName: 'messages',
timestamps: true,
indexes: [
{ fields: ['conversationId'] },
{ fields: ['role'] },
{ fields: ['createdAt'] },
{ fields: ['conversationId', 'createdAt'] },
],
});
module.exports = Message;

43
server/models/User.js Normal file
View file

@ -0,0 +1,43 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true,
},
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
preferences: {
type: DataTypes.JSONB,
defaultValue: {
theme: 'light',
notifications: true,
defaultAssistant: 'smart-goals',
},
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
}, {
tableName: 'users',
timestamps: true,
indexes: [
{ fields: ['email'] },
{ fields: ['isActive'] },
],
});
module.exports = User;

22
server/models/index.js Normal file
View file

@ -0,0 +1,22 @@
const { sequelize } = require('../config/database');
const User = require('./User');
const Assistant = require('./Assistant');
const Conversation = require('./Conversation');
const Message = require('./Message');
User.hasMany(Conversation, { foreignKey: 'userId', as: 'conversations' });
Conversation.belongsTo(User, { foreignKey: 'userId', as: 'user' });
Assistant.hasMany(Conversation, { foreignKey: 'assistantId', as: 'conversations' });
Conversation.belongsTo(Assistant, { foreignKey: 'assistantId', as: 'assistant' });
Conversation.hasMany(Message, { foreignKey: 'conversationId', as: 'messages' });
Message.belongsTo(Conversation, { foreignKey: 'conversationId', as: 'conversation' });
module.exports = {
sequelize,
User,
Assistant,
Conversation,
Message,
};

View file

@ -0,0 +1,86 @@
const express = require('express');
const { Assistant } = require('../models');
const router = express.Router();
router.get('/', async (req, res, next) => {
try {
const { category, isActive = 'true' } = req.query;
const whereClause = { isActive: isActive === 'true' };
if (category) {
whereClause.category = category;
}
const agents = await Assistant.findAll({
where: whereClause,
order: [['sortOrder', 'ASC'], ['name', 'ASC']],
attributes: {
exclude: ['systemPrompt', 'createdAt', 'updatedAt']
}
});
const groupedByCategory = agents.reduce((acc, agent) => {
const cat = agent.category;
if (!acc[cat]) {
acc[cat] = [];
}
acc[cat].push(agent);
return acc;
}, {});
res.json({
agents: agents.map(a => ({
id: a.id,
key: a.key,
name: a.name,
description: a.description,
category: a.category,
model: a.model,
temperature: a.temperature,
maxTokens: a.maxTokens,
metadata: a.metadata,
})),
groupedByCategory,
total: agents.length,
});
} catch (error) {
next(error);
}
});
router.get('/:key', async (req, res, next) => {
try {
const { key } = req.params;
const agent = await Assistant.findOne({
where: { key, isActive: true }
});
if (!agent) {
return res.status(404).json({
error: 'Agent Not Found',
message: `Agent with key '${key}' not found or inactive`
});
}
res.json({
id: agent.id,
key: agent.key,
name: agent.name,
description: agent.description,
category: agent.category,
model: agent.model,
temperature: agent.temperature,
maxTokens: agent.maxTokens,
tools: agent.tools,
metadata: agent.metadata,
});
} catch (error) {
next(error);
}
});
module.exports = router;

360
server/routes/chat.js Normal file
View file

@ -0,0 +1,360 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { Assistant, Conversation, Message, User } = require('../models');
const openaiService = require('../utils/openai');
const { chatLimiter } = require('../middleware/rateLimiter');
const router = express.Router();
router.use(chatLimiter);
// Helper function to generate title for first conversation
async function generateTitleIfNeeded(conversation, userMessage, assistantResponse, agentName) {
try {
// Check if this conversation needs a title (first exchange)
const messageCount = await Message.count({
where: { conversationId: conversation.id }
});
if (messageCount === 2 && !conversation.title) {
console.log(`Generating title for conversation ${conversation.id}`);
const title = await openaiService.generateConversationTitle(
userMessage,
assistantResponse,
agentName
);
await conversation.update({ title });
console.log(`Updated conversation ${conversation.id} with title: "${title}"`);
}
} catch (error) {
console.error('Error generating conversation title:', error);
// Don't throw - title generation failure shouldn't break chat
}
}
router.post('/completions', async (req, res, next) => {
try {
let {
messages,
assistantKey = 'smart-goals',
conversationId,
userId,
stream = false
} = req.body;
// Ensure we have a valid UUID for userId
if (!userId || typeof userId !== 'string' || !userId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
userId = uuidv4(); // Generate a proper UUID
console.log(`Generated new userId: ${userId}`);
}
// Ensure user exists or create one
let user = await User.findByPk(userId);
if (!user) {
user = await User.create({
id: userId,
email: `user-${userId}@temp.com`,
name: `User ${userId.substring(0, 8)}`,
preferences: { theme: 'light', notifications: true, defaultAssistant: assistantKey }
});
console.log(`Created new user: ${user.id}`);
}
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({
error: 'Validation Error',
message: 'Messages array is required and cannot be empty'
});
}
const agent = await Assistant.findOne({
where: { key: assistantKey, isActive: true }
});
if (!agent) {
return res.status(404).json({
error: 'Agent Not Found',
message: `Agent with key '${assistantKey}' not found or inactive`
});
}
let conversation;
let conversationHistory = [];
if (conversationId) {
conversation = await Conversation.findByPk(conversationId);
if (!conversation) {
return res.status(404).json({
error: 'Conversation Not Found',
message: 'Specified conversation not found'
});
}
// Load existing conversation history for context
const existingMessages = await Message.findAll({
where: { conversationId },
order: [['createdAt', 'ASC']]
});
conversationHistory = existingMessages.map(msg => ({
role: msg.role,
content: msg.content
}));
console.log(`Loaded ${conversationHistory.length} existing messages for conversation ${conversationId}`);
} else {
conversation = await Conversation.create({
id: uuidv4(),
userId,
assistantId: agent.id,
status: 'active',
lastMessageAt: new Date(),
});
}
// Build complete message history: system prompt + conversation history + new messages
const formattedMessages = [
openaiService.buildSystemMessage(agent.systemPrompt),
...conversationHistory,
...openaiService.formatMessagesForAPI(messages)
];
console.log(`Total messages for OpenAI: ${formattedMessages.length} (${conversationHistory.length} history + ${messages.length} new + 1 system)`);
if (stream) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
const streamResponse = await openaiService.createStreamingResponse(
formattedMessages,
{
model: agent.model,
temperature: agent.temperature,
maxTokens: agent.maxTokens,
}
);
let fullResponse = '';
for await (const chunk of streamResponse) {
const delta = chunk.choices[0]?.delta?.content || '';
if (delta) {
fullResponse += delta;
res.write(`data: ${JSON.stringify({ content: delta, done: false })}\n\n`);
}
}
await Message.create({
conversationId: conversation.id,
role: 'user',
content: messages[messages.length - 1].content,
});
await Message.create({
conversationId: conversation.id,
role: 'assistant',
content: fullResponse,
model: agent.model,
metadata: { assistantKey },
});
await conversation.update({ lastMessageAt: new Date() });
// Generate title for first conversation
await generateTitleIfNeeded(
conversation,
messages[messages.length - 1].content,
fullResponse,
agent.name
);
res.write(`data: ${JSON.stringify({
content: '',
done: true,
conversationId: conversation.id,
totalTokens: null
})}\n\n`);
res.end();
} else {
const response = await openaiService.createResponse(
formattedMessages,
{
model: agent.model,
temperature: agent.temperature,
maxTokens: agent.maxTokens,
}
);
const assistantResponse = response.choices[0].message.content;
await Message.create({
conversationId: conversation.id,
role: 'user',
content: messages[messages.length - 1].content,
});
await Message.create({
conversationId: conversation.id,
role: 'assistant',
content: assistantResponse,
model: agent.model,
tokenCount: response.usage?.completion_tokens,
finishReason: response.choices[0].finish_reason,
metadata: { assistantKey },
});
await conversation.update({ lastMessageAt: new Date() });
// Generate title for first conversation
await generateTitleIfNeeded(
conversation,
messages[messages.length - 1].content,
assistantResponse,
agent.name
);
res.json({
response: assistantResponse,
conversationId: conversation.id,
usage: response.usage,
model: agent.model,
finishReason: response.choices[0].finish_reason,
});
}
} catch (error) {
console.error('Chat completion error:', error);
if (error.code === 'insufficient_quota') {
return res.status(402).json({
error: 'Quota Exceeded',
message: 'OpenAI API quota exceeded. Please check your billing.'
});
}
if (error.code === 'rate_limit_exceeded') {
return res.status(429).json({
error: 'Rate Limit Exceeded',
message: 'OpenAI API rate limit exceeded. Please try again later.'
});
}
error.code = 'OPENAI_API_ERROR';
next(error);
}
});
router.get('/conversations', async (req, res, next) => {
try {
const { userId, limit = 20, offset = 0 } = req.query;
const whereClause = {};
if (userId) {
whereClause.userId = userId;
}
const conversations = await Conversation.findAll({
where: { ...whereClause, status: 'active' },
include: [
{
model: Assistant,
as: 'assistant',
attributes: ['key', 'name', 'description']
}
],
order: [['lastMessageAt', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset),
});
res.json({
conversations: conversations.map(conv => ({
id: conv.id,
title: conv.title,
lastMessageAt: conv.lastMessageAt,
createdAt: conv.createdAt,
assistant: {
key: conv.assistant.key,
name: conv.assistant.name,
description: conv.assistant.description
}
})),
total: conversations.length
});
} catch (error) {
next(error);
}
});
router.get('/conversations/:conversationId/messages', async (req, res, next) => {
try {
const { conversationId } = req.params;
const { limit = 50, offset = 0 } = req.query;
const conversation = await Conversation.findByPk(conversationId);
if (!conversation) {
return res.status(404).json({
error: 'Conversation Not Found',
message: 'Specified conversation not found'
});
}
const messages = await Message.findAll({
where: { conversationId },
order: [['createdAt', 'ASC']],
limit: parseInt(limit),
offset: parseInt(offset),
});
res.json({
messages: messages.map(msg => ({
id: msg.id,
role: msg.role,
content: msg.content,
createdAt: msg.createdAt,
metadata: msg.metadata,
})),
conversationId,
total: messages.length,
});
} catch (error) {
next(error);
}
});
router.delete('/conversations/:conversationId', async (req, res, next) => {
try {
const { conversationId } = req.params;
const conversation = await Conversation.findByPk(conversationId);
if (!conversation) {
return res.status(404).json({
error: 'Conversation Not Found',
message: 'Specified conversation not found'
});
}
// Soft delete - change status to 'deleted' but preserve data
await conversation.update({
status: 'deleted',
updatedAt: new Date()
});
res.json({
message: 'Conversation deleted successfully',
conversationId: conversation.id
});
} catch (error) {
console.error('Error deleting conversation:', error);
next(error);
}
});
module.exports = router;

135
server/utils/openai.js Normal file
View file

@ -0,0 +1,135 @@
const OpenAI = require('openai');
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
organization: process.env.OPENAI_ORG_ID,
});
class OpenAIService {
constructor() {
if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY.includes('your-actual')) {
console.warn('⚠️ OpenAI API key not configured properly');
}
}
async createResponse(messages, assistantConfig = {}) {
try {
const {
model = 'gpt-4o',
temperature = 0.7,
maxTokens = 4000,
stream = false,
} = assistantConfig;
const response = await openai.chat.completions.create({
model,
messages,
temperature,
max_tokens: maxTokens,
stream,
});
return response;
} catch (error) {
console.error('OpenAI Responses API Error:', error);
throw error;
}
}
async createStreamingResponse(messages, assistantConfig = {}) {
try {
const {
model = 'gpt-4o',
temperature = 0.7,
maxTokens = 4000,
} = assistantConfig;
const stream = await openai.chat.completions.create({
model,
messages,
temperature,
max_tokens: maxTokens,
stream: true,
});
return stream;
} catch (error) {
console.error('OpenAI Streaming Responses API Error:', error);
throw error;
}
}
formatMessagesForAPI(messages) {
return messages.map(msg => ({
role: msg.role,
content: msg.content,
}));
}
buildSystemMessage(systemPrompt) {
return {
role: 'system',
content: systemPrompt,
};
}
async generateConversationTitle(userMessage, assistantResponse, agentName) {
try {
if (!process.env.ENABLE_TITLE_GENERATION || process.env.ENABLE_TITLE_GENERATION !== 'true') {
console.log('Title generation disabled, using fallback');
return `Chat with ${agentName}`;
}
const titlePrompt = `Based on this conversation between a user and an AI assistant called "${agentName}", generate a short, descriptive title (3-6 words) that captures what the user is asking about or discussing. Be specific and concise.
User message: "${userMessage}"
Assistant response: "${assistantResponse}"
Generate only the title, nothing else. Examples:
- "Marketing Campaign Ideas"
- "JavaScript Function Help"
- "Travel Planning Advice"
- "Budget Analysis Discussion"`;
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini', // Use cheaper model for title generation
messages: [
{ role: 'user', content: titlePrompt }
],
temperature: 0.3, // Lower temperature for more consistent titles
max_tokens: 20, // Short titles only
});
const title = response.choices[0]?.message?.content?.trim();
if (!title) {
console.warn('No title generated, using fallback');
return `Chat with ${agentName}`;
}
// Clean up the title - remove quotes if present
const cleanTitle = title.replace(/^["']|["']$/g, '');
console.log(`Generated title: "${cleanTitle}"`);
return cleanTitle;
} catch (error) {
console.error('Error generating conversation title:', error);
return `Chat with ${agentName}`; // Fallback title
}
}
async testConnection() {
try {
const response = await this.createResponse([
{ role: 'user', content: 'Hello, this is a test message.' }
], { maxTokens: 50 });
console.log('✅ OpenAI Responses API connection test successful');
return { success: true, response };
} catch (error) {
console.error('❌ OpenAI Responses API connection test failed:', error.message);
return { success: false, error: error.message };
}
}
}
module.exports = new OpenAIService();