Compare commits
2 Commits
d0129b57dd
...
db2e7d8017
| Author | SHA1 | Date | |
|---|---|---|---|
| db2e7d8017 | |||
| 45dfbc42d3 |
@@ -15,12 +15,13 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useRef, useEffect, useState } from 'react';
|
import { useRef, useEffect, useState } from 'react';
|
||||||
import { IconVolume, IconMicrophone, IconNotes } from '@tabler/icons-react';
|
import { IconVolume, IconMicrophone, IconDeviceFloppy } from '@tabler/icons-react';
|
||||||
import { UserMenu } from '@/components/UserMenu';
|
import { UserMenu } from '@/components/UserMenu';
|
||||||
import { useVoiceMode } from '@/hooks/useVoiceMode';
|
import { useVoiceMode } from '@/hooks/useVoiceMode';
|
||||||
import { useAppMachine } from '@/hooks/useAppMachine';
|
import { useAppMachine } from '@/hooks/useAppMachine';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
|
import { useSelector } from '@xstate/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the voice button text based on the current state
|
* Get the voice button text based on the current state
|
||||||
@@ -62,6 +63,10 @@ export default function ChatPage() {
|
|||||||
// State for creating node
|
// State for creating node
|
||||||
const [isCreatingNode, setIsCreatingNode] = useState(false);
|
const [isCreatingNode, setIsCreatingNode] = useState(false);
|
||||||
|
|
||||||
|
// Check if we have a pending draft (using useSelector for reactivity)
|
||||||
|
const pendingNodeDraft = useSelector(appActor, (state) => state.context.pendingNodeDraft);
|
||||||
|
const hasPendingDraft = !!pendingNodeDraft;
|
||||||
|
|
||||||
// Use the clean voice mode hook
|
// Use the clean voice mode hook
|
||||||
const { state, send, transcript, error } = useVoiceMode({
|
const { state, send, transcript, error } = useVoiceMode({
|
||||||
messages,
|
messages,
|
||||||
@@ -122,6 +127,34 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handler for Manual/Save button
|
||||||
|
const handleManualOrSave = () => {
|
||||||
|
if (hasPendingDraft && pendingNodeDraft) {
|
||||||
|
// If we have a draft, navigate to edit with it
|
||||||
|
appActor.send({
|
||||||
|
type: 'NAVIGATE_TO_EDIT',
|
||||||
|
draft: pendingNodeDraft,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create an empty draft for manual entry
|
||||||
|
const emptyDraft = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
conversationContext: messages.map((m) => {
|
||||||
|
if ('parts' in m && Array.isArray((m as any).parts)) {
|
||||||
|
return `${m.role}: ${(m as any).parts.find((p: any) => p.type === 'text')?.text || ''}`;
|
||||||
|
}
|
||||||
|
return `${m.role}: ${(m as any).content || ''}`;
|
||||||
|
}).join('\n'),
|
||||||
|
};
|
||||||
|
|
||||||
|
appActor.send({
|
||||||
|
type: 'CREATE_NODE_FROM_CONVERSATION',
|
||||||
|
draft: emptyDraft,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Add initial greeting message on first load
|
// Add initial greeting message on first load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
@@ -377,6 +410,15 @@ export default function ChatPage() {
|
|||||||
variant="filled"
|
variant="filled"
|
||||||
disabled={isVoiceActive}
|
disabled={isVoiceActive}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleManualOrSave}
|
||||||
|
radius="xl"
|
||||||
|
variant={hasPendingDraft ? 'filled' : 'light'}
|
||||||
|
color={hasPendingDraft ? 'blue' : 'gray'}
|
||||||
|
leftSection={<IconDeviceFloppy size={20} />}
|
||||||
|
>
|
||||||
|
{hasPendingDraft ? 'Save Draft' : 'Save'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"test": "npx magnitude"
|
"test": "npx magnitude",
|
||||||
|
"schema:apply": "node scripts/apply-schema.js",
|
||||||
|
"schema:deploy": "./scripts/deploy-schema.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/google": "latest",
|
"@ai-sdk/google": "latest",
|
||||||
|
|||||||
105
scripts/apply-schema-prod.js
Executable file
105
scripts/apply-schema-prod.js
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply SurrealDB schema to production database
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* SURREALDB_URL=<url> SURREALDB_USER=<user> SURREALDB_PASS=<pass> \
|
||||||
|
* SURREALDB_NS=<ns> SURREALDB_DB=<db> SURREALDB_JWT_SECRET=<secret> \
|
||||||
|
* node scripts/apply-schema-prod.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Surreal = require('surrealdb').default;
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function applySchema() {
|
||||||
|
// Read config from environment
|
||||||
|
const config = {
|
||||||
|
url: process.env.SURREALDB_URL,
|
||||||
|
username: process.env.SURREALDB_USER,
|
||||||
|
password: process.env.SURREALDB_PASS,
|
||||||
|
namespace: process.env.SURREALDB_NS,
|
||||||
|
database: process.env.SURREALDB_DB,
|
||||||
|
jwtSecret: process.env.SURREALDB_JWT_SECRET,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate required config
|
||||||
|
const missing = Object.entries(config)
|
||||||
|
.filter(([_, value]) => !value)
|
||||||
|
.map(([key]) => key);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error('[Schema] ✗ Missing required environment variables:', missing.join(', '));
|
||||||
|
console.error('[Schema] Please set all required variables:');
|
||||||
|
console.error('[Schema] SURREALDB_URL, SURREALDB_USER, SURREALDB_PASS,');
|
||||||
|
console.error('[Schema] SURREALDB_NS, SURREALDB_DB, SURREALDB_JWT_SECRET');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Surreal();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Schema] Connecting to SurrealDB...');
|
||||||
|
console.log(`[Schema] URL: ${config.url}`);
|
||||||
|
await db.connect(config.url);
|
||||||
|
|
||||||
|
console.log('[Schema] Signing in...');
|
||||||
|
await db.signin({
|
||||||
|
username: config.username,
|
||||||
|
password: config.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Schema] Using namespace and database...');
|
||||||
|
console.log(`[Schema] Namespace: ${config.namespace}`);
|
||||||
|
console.log(`[Schema] Database: ${config.database}`);
|
||||||
|
await db.use({
|
||||||
|
namespace: config.namespace,
|
||||||
|
database: config.database,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Schema] Reading schema file...');
|
||||||
|
const schemaPath = path.join(__dirname, '..', 'db', 'schema.surql');
|
||||||
|
let schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||||
|
|
||||||
|
// Replace $env.SURREALDB_JWT_SECRET with actual value
|
||||||
|
schema = schema.replace('$env.SURREALDB_JWT_SECRET', `'${config.jwtSecret}'`);
|
||||||
|
|
||||||
|
console.log('[Schema] Executing schema...');
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await db.query(schema);
|
||||||
|
} catch (error) {
|
||||||
|
// If error contains "already exists", it's OK - schema was already applied
|
||||||
|
if (error.message.includes('already exists')) {
|
||||||
|
console.log('[Schema] ⚠ Some schema elements already exist (this is OK)');
|
||||||
|
console.log('[Schema] ✓ Schema is up to date!');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
console.log(`[Schema] Executed ${result.length} queries`);
|
||||||
|
|
||||||
|
// Log any errors
|
||||||
|
result.forEach((r, i) => {
|
||||||
|
if (r.status === 'ERR') {
|
||||||
|
console.error(`[Schema] Error in query ${i + 1}:`, r.result);
|
||||||
|
} else {
|
||||||
|
console.log(`[Schema] ✓ Query ${i + 1} succeeded`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Schema] ✓ Schema applied successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Schema] ✗ Failed to apply schema:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applySchema();
|
||||||
23
scripts/deploy-schema.sh
Executable file
23
scripts/deploy-schema.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Deploy schema to production SurrealDB
|
||||||
|
# This script reads from .prod.env and applies the schema
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ ! -f .prod.env ]; then
|
||||||
|
echo "Error: .prod.env file not found"
|
||||||
|
echo "Please create .prod.env with your production database credentials"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Loading production environment variables..."
|
||||||
|
export $(cat .prod.env | grep -v '^#' | grep '=' | xargs)
|
||||||
|
|
||||||
|
echo "Applying schema to production database..."
|
||||||
|
node scripts/apply-schema-prod.js
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Schema deployment complete!"
|
||||||
|
echo ""
|
||||||
|
echo "You can now test your deployment at: https://www.ponderants.com"
|
||||||
Reference in New Issue
Block a user