sorin-s-cat / index.html
ninjacricket's picture
undefined - Initial Deployment
f20e37d verified
raw
history blame
95.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BAIRC-INTERNAL-COM-NETWORK</title>
<style>
/* CSS Variables for theming */
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--accent: #00d4aa;
--accent-hover: #00b894;
--border: #404040;
--danger: #e74c3c;
--warning: #f39c12;
--success: #27ae60;
/* Custom colors for generated logs */
--log-console-border: #007bff; /* Blue */
--log-email-border: #ff8c00; /* Dark Orange */
--log-dream-border: #8a2be2; /* BlueViolet */
--log-internal-border: #dc3545; /* Red */
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow: hidden;
}
/* Layout Components */
.app-container {
display: flex;
height: 100vh;
}
.sidebar {
width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: width 0.3s;
}
.conversations-sidebar {
width: 260px;
background: var(--bg-primary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-views-container {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar Sections */
.sidebar-section {
padding: 1rem;
border-bottom: 1px solid var(--border);
overflow-y: auto;
}
.sidebar-section.no-grow {
flex-grow: 0;
flex-shrink: 0;
}
.sidebar-section.grow {
flex-grow: 1;
}
.sidebar-section h3 {
margin-bottom: 0.75rem;
color: var(--accent);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Form Controls */
.form-group {
margin-bottom: 0.75rem;
}
.form-group label {
display: block;
margin-bottom: 0.25rem;
font-size: 0.85rem;
color: var(--text-secondary);
}
.form-control {
width: 100%;
padding: 0.5rem;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.85rem;
}
.form-control:focus {
outline: none;
border-color: var(--accent);
}
textarea.form-control {
resize: vertical;
min-height: 80px;
}
/* Buttons */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--border);
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.btn-icon {
background: none;
border: none;
color: var(--text-secondary);
padding: 0.25rem;
cursor: pointer;
border-radius: 4px;
}
.btn-icon:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
/* Main Chat Interface */
.chat-header {
padding: 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.chat-title {
font-size: 1.1rem;
font-weight: 600;
}
.chat-actions {
display: flex;
gap: 0.5rem;
}
.chat-view {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
/* Added for split view */
border-right: 1px solid var(--border);
}
.chat-view:last-child {
border-right: none;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.message {
margin-bottom: 1.5rem;
padding: 1rem;
border-radius: 8px;
max-width: 90%;
word-wrap: break-word;
position: relative;
}
.message.user {
background: var(--bg-secondary);
margin-left: auto;
border: 1px solid var(--border);
}
.message.assistant {
background: var(--bg-tertiary);
margin-right: auto;
}
/* Specific borders for generated log messages */
.message.assistant.log-console {
border: 2px solid var(--log-console-border);
}
.message.assistant.log-email {
border: 2px solid var(--log-email-border);
}
.message.assistant.log-dream {
border: 2px solid var(--log-dream-border);
}
.message.assistant.log-internal {
border: 2px solid var(--log-internal-border);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.8rem;
color: var(--text-secondary);
}
.message-content {
color: var(--text-primary);
}
.message-content pre {
background: var(--bg-primary);
padding: 0.75rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.message-content code {
font-family: "Courier New", Courier, monospace;
}
.message-content code.language-inline {
background: var(--bg-primary);
padding: 0.1em 0.3em;
border-radius: 3px;
}
.message-actions {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: none;
gap: 0.25rem;
background: var(--bg-secondary);
padding: 0.25rem;
border-radius: 5px;
border: 1px solid var(--border);
}
.message:hover .message-actions {
display: flex;
}
/* Input Area */
.input-area {
padding: 1rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
}
.input-container {
display: flex;
gap: 0.5rem;
align-items: flex-end;
}
.input-field-wrapper {
flex: 1;
display: flex;
flex-direction: column;
}
.input-field {
flex: 1;
min-height: 80px;
max-height: 200px;
resize: vertical;
}
.input-toolbar {
display: flex;
justify-content: flex-end;
padding-top: 0.25rem;
font-size: 0.75rem;
color: var(--text-secondary);
}
/* List Item Styling (for Prompts, Conversations, Notes) */
.list-item {
padding: 0.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.list-item:hover {
border-color: var(--accent);
}
.list-item.active {
border-color: var(--accent);
background: rgba(0, 212, 170, 0.1);
}
.list-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.list-item-name {
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list-item-actions {
display: flex;
gap: 0.25rem;
}
.list-item-preview {
font-size: 0.8rem;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Utility Classes */
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.mb-1 {
margin-bottom: 1rem;
}
/* Loading Animation */
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--border);
border-radius: 50%;
border-top-color: var(--accent);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
/* Modal/Lightbox Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition:
opacity 0.3s,
visibility 0.3s;
}
.modal-overlay.visible {
opacity: 1;
visibility: visible;
}
.modal-content {
background: var(--bg-secondary);
padding: 2rem;
border-radius: 8px;
width: 90%;
max-width: 600px;
border: 1px solid var(--border);
transform: scale(0.95);
transition: transform 0.3s;
}
.modal-overlay.visible .modal-content {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-title {
font-size: 1.25rem;
color: var(--accent);
}
.modal-close {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
}
.modal-body .form-group {
margin-bottom: 1rem;
}
.modal-body textarea.form-control {
min-height: 200px;
}
.modal-footer {
margin-top: 1.5rem;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
/* Empty State */
#empty-state {
display: none;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem;
color: var(--text-secondary);
}
/* Generate Logs Section */
.generate-logs {
padding: 1rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.generate-logs h3 {
width: 100%;
text-align: center;
color: var(--text-secondary);
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Conversations & Notebook Sidebar -->
<div class="conversations-sidebar">
<div class="sidebar-section grow" style="display: flex; flex-direction: column;">
<div class="flex-between mb-1">
<h3>💬 Conversations</h3>
<div class="chat-actions">
<button
class="btn btn-secondary btn-small"
onclick="newConversation()"
title="New Conversation"
>
+
</button>
<button
class="btn btn-secondary btn-small"
onclick="toggleSplitView()"
title="Split View"
>
</button>
<button
class="btn btn-secondary btn-small"
onclick="exportBRF()"
title="Export BRF Data"
>
💾
</button>
<label
for="import-brf-file"
class="btn btn-secondary btn-small"
title="Import BRF Data"
>
📂
</label>
<input
type="file"
id="import-brf-file"
accept=".json"
style="display: none;"
onchange="handleBRFImport(event)"
/>
</div>
</div>
<div id="conversations-list" style="flex: 1; overflow-y: auto;"></div>
</div>
<div class="sidebar-section no-grow">
<div class="flex-between mb-1">
<h3>📒 Notebook</h3>
<button
class="btn btn-secondary btn-small"
onclick="openNoteModal()"
title="New Note"
>
+
</button>
</div>
<div id="notebook-list" style="max-height: 150px; overflow-y: auto;"></div>
</div>
</div>
<!-- Parameters & Prompts Sidebar -->
<div class="sidebar">
<!-- Model Parameters -->
<div class="sidebar-section no-grow">
<h3>⚙️ Parameters</h3>
<div class="form-group">
<label for="model">Model</label>
<select id="model" class="form-control">
<option value="deepseek-chat">deepseek-chat</option>
<option value="deepseek-coder">deepseek-coder</option>
</select>
</div>
<div class="form-group">
<label>Temperature: <span id="temp-value">0.7</span></label>
<input
type="range"
id="temperature"
class="form-control"
min="0"
max="2"
step="0.1"
value="0.7"
/>
</div>
<div class="form-group">
<label>Max Tokens</label>
<input
type="number"
id="max-tokens"
class="form-control"
value="2048"
min="1"
/>
</div>
</div>
<!-- System Prompts -->
<div class="sidebar-section grow" style="display: flex; flex-direction: column;">
<div class="flex-between mb-1">
<h3>📝 System Prompts</h3>
<button
class="btn btn-secondary btn-small"
onclick="openPromptModal()"
title="New Prompt"
>
+
</button>
</div>
<div id="prompts-list" style="flex: 1; overflow-y: auto;"></div>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<div id="empty-state" style="display: none;">
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
Welcome to the Chat Studio
</h2>
<p style="margin-bottom: 1.5rem;">
Select a conversation or create a new one to get started.
</p>
<button class="btn btn-primary" onclick="newConversation()">
+ New Conversation
</button>
</div>
<div id="chat-views-container" class="chat-views-container" style="display: none;">
<div id="chat-view-1" class="chat-view">
<!-- Chat Header -->
<div class="chat-header">
<div id="chat-title-1" class="chat-title"></div>
<div class="chat-actions">
<!-- New prompt selector for chat view 1 -->
<select
id="system-prompt-select-1"
class="form-control"
onchange="selectSystemPrompt(this.value, 1)"
style="width: auto; max-width: 180px;"
></select>
<button
class="btn btn-secondary btn-small"
onclick="exportChatMD(1)"
title="Export Conversation to Markdown"
>
Export MD
</button>
<button
class="btn btn-danger btn-small"
onclick="deleteConversation(AppState.activeChatViews[0].conversationId)"
title="Delete Conversation"
>
🗑️ Delete
</button>
</div>
</div>
<!-- Messages -->
<div class="messages-container" id="messages-1"></div>
<!-- Input Area -->
<div class="input-area">
<div class="input-container">
<div class="input-field-wrapper">
<textarea
id="user-input-1"
class="form-control input-field"
placeholder="Type your message... (Shift+Enter for new line)"
rows="3"
></textarea>
<div class="input-toolbar">
<span id="token-counter-1">Tokens: 0</span>
</div>
</div>
<button
class="btn btn-primary"
onclick="sendMessage(1)"
id="send-btn-1"
>
Send
</button>
</div>
<!-- Generate Logs Section -->
<div class="generate-logs">
<h3>Logs</h3>
<button
class="btn btn-primary"
onclick="generateLog(1, this)"
data-prompt-id="console-output-generator"
>
CONSOLE Output
</button>
<button
class="btn btn-primary"
onclick="generateLog(1, this)"
data-prompt-id="email-generator"
>
EMAILs
</button>
<button
class="btn btn-primary"
onclick="generateLog(1, this)"
data-prompt-id="dream-log-generator"
>
DREAM log
</button>
<button
class="btn btn-primary"
onclick="generateLog(1, this)"
data-prompt-id="internal-monologue-generator"
>
INTERNAL Monologue
</button>
</div>
</div>
</div>
<div id="chat-view-2" class="chat-view" style="display: none;">
<!-- Chat Header -->
<div class="chat-header">
<div id="chat-title-2" class="chat-title"></div>
<div class="chat-actions">
<!-- New prompt selector for chat view 2 -->
<select
id="system-prompt-select-2"
class="form-control"
onchange="selectSystemPrompt(this.value, 2)"
style="width: auto; max-width: 180px;"
></select>
<button
class="btn btn-secondary btn-small"
onclick="exportChatMD(2)"
title="Export Conversation to Markdown"
>
Export MD
</button>
<button
class="btn btn-danger btn-small"
onclick="deleteConversation(AppState.activeChatViews[1].conversationId)"
title="Delete Conversation"
>
🗑️ Delete
</button>
</div>
</div>
<!-- Messages -->
<div class="messages-container" id="messages-2"></div>
<!-- Input Area -->
<div class="input-area">
<div class="input-container">
<div class="input-field-wrapper">
<textarea
id="user-input-2"
class="form-control input-field"
placeholder="Type your message... (Shift+Enter for new line)"
rows="3"
></textarea>
<div class="input-toolbar">
<span id="token-counter-2">Tokens: 0</span>
</div>
</div>
<button
class="btn btn-primary"
onclick="sendMessage(2)"
id="send-btn-2"
>
Send
</button>
</div>
<!-- Generate Logs Section -->
<div class="generate-logs">
<h3>Logs</h3>
<button
class="btn btn-primary"
onclick="generateLog(2, this)"
data-prompt-id="console-output-generator"
>
CONSOLE Output
</button>
<button
class="btn btn-primary"
onclick="generateLog(2, this)"
data-prompt-id="email-generator"
>
EMAILs
</button>
<button
class="btn btn-primary"
onclick="generateLog(2, this)"
data-prompt-id="dream-log-generator"
>
DREAM log
</button>
<button
class="btn btn-primary"
onclick="generateLog(2, this)"
data-prompt-id="internal-monologue-generator"
>
INTERNAL Monologue
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Prompt Editor Modal -->
<div id="prompt-modal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 id="prompt-modal-title" class="modal-title">Edit System Prompt</h2>
<button class="modal-close" onclick="closePromptModal()">
×
</button>
</div>
<div class="modal-body">
<input type="hidden" id="modal-prompt-id" value="" />
<div class="form-group">
<label for="modal-prompt-name">Prompt Name</label>
<input
type="text"
id="modal-prompt-name"
class="form-control"
placeholder="e.g., Sarcastic Pirate Assistant"
/>
</div>
<div class="form-group">
<label for="modal-prompt-content">Prompt Content</label>
<textarea
id="modal-prompt-content"
class="form-control"
rows="10"
placeholder="You are a helpful assistant..."
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closePromptModal()">
Cancel
</button>
<button class="btn btn-primary" onclick="savePromptFromModal()">
Save Prompt
</button>
</div>
</div>
</div>
<!-- Note Editor Modal -->
<div id="note-modal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 id="note-modal-title" class="modal-title">Create New Note</h2>
<button class="modal-close" onclick="closeNoteModal()">
×
</button>
</div>
<div class="modal-body">
<input type="hidden" id="modal-note-id" value="" />
<div class="form-group">
<label for="modal-note-content">Note Content</label>
<textarea
id="modal-note-content"
class="form-control"
rows="10"
placeholder="Your note here..."
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeNoteModal()">
Cancel
</button>
<button class="btn btn-primary" onclick="saveNoteFromModal()">
Save Note
</button>
</div>
</div>
</div>
<script>
// BAIRC Import/Export Standard Implementation
class BRFManager {
constructor(appState) {
this.version = "1.0";
this.appState = appState; // Pass AppState instance to the manager
this.requiredFields = [
"brf_version",
"export_metadata",
"conversations",
"system_prompts",
"user_psychological_profile",
"session_metadata",
"validation",
];
}
// --- Validation Methods ---
validateBRF(data) {
const errors = [];
// Basic structural validation
this.requiredFields.forEach((field) => {
if (!data.hasOwnProperty(field)) {
errors.push(`Missing required field: ${field}`);
}
});
if (errors.length > 0) return { isValid: false, errors };
// Check version compatibility
if (data.brf_version !== this.version) {
errors.push(
`Unsupported BRF version: Expected ${this.version}, got ${data.brf_version}`,
);
}
// Validate timestamps (more robust checks, though basic ISO8601 is covered by schema format)
const timestampFields = [
"export_metadata.timestamp",
"user_psychological_profile.created_at",
"user_psychological_profile.last_updated",
];
timestampFields.forEach((path) => {
const value = this.getNestedValue(data, path);
if (value && !this.isValidISO8601(value)) {
errors.push(`Invalid timestamp format for ${path}: "${value}"`);
}
});
// Validate psychological metrics (0-10 scale)
const psychMetrics =
data.user_psychological_profile?.emotional_patterns;
if (psychMetrics) {
Object.entries(psychMetrics).forEach(([key, value]) => {
// Allow string values if they are explicitly handled elsewhere, otherwise require number
if (typeof value === "number" && (value < 0 || value > 10)) {
errors.push(
`Psychological metric out of range (0-10): ${key} = ${value}`,
);
}
});
}
// Basic consistency checks (can be expanded)
if (data.conversations && typeof data.conversations !== "object") {
errors.push("Conversations field must be an object.");
}
if (data.system_prompts && typeof data.system_prompts !== "object") {
errors.push("System prompts field must be an object.");
}
if (
data.user_psychological_profile &&
typeof data.user_psychological_profile !== "object"
) {
errors.push("User psychological profile must be an object.");
}
return {
isValid: errors.length === 0,
errors: errors,
};
}
// --- Export Methods ---
generateBRF(options = {}) {
const timestamp = new Date().toISOString();
const exportId = `brf_${timestamp.replace(/[:.]/g, "")}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const currentData = this.getCurrentData(); // Get data from AppState
const brf = {
brf_version: this.version,
export_metadata: {
timestamp: timestamp,
studio_version: options.studioVersion || "BAIRC Chat Studio 2.1.0", // Hardcoded for this app
export_type: options.type || "full_backup",
exported_by: options.userId || "anonymous_user",
export_id: exportId,
compression: options.minified ? "minified" : "standard",
},
conversations: this.formatConversations(currentData.conversations),
system_prompts: this.formatSystemPrompts(
currentData.systemPrompts,
),
user_psychological_profile: this.formatPsychProfile(
currentData.psychProfile,
),
session_metadata: this.formatSessionMetadata(currentData.metadata),
// The validation hash is calculated AFTER the BRF object is fully formed
validation: null, // Placeholder, calculated later
};
// Calculate validation hash on the stringified data (without the validation field itself)
const stringifiedBrfForHash = JSON.stringify({
...brf,
validation: undefined,
});
brf.validation = this.generateValidationHash(stringifiedBrfForHash);
return options.minified
? JSON.stringify(brf)
: JSON.stringify(brf, null, 2);
}
// --- Import Methods ---
async importBRF(brfContent, options = {}) {
try {
const data =
typeof brfContent === "string" ?
JSON.parse(brfContent) :
brfContent;
// Validate format
const validation = this.validateBRF(data);
if (!validation.isValid) {
throw new Error(
`Invalid BRF format: ${validation.errors.join(", ")}`,
);
}
// Verify hash (optional but recommended for integrity)
const receivedHash = data.validation?.sha256;
const dataForHashCheck = JSON.stringify({
...data,
validation: undefined,
});
if (receivedHash && this.simpleHash(dataForHashCheck) !== receivedHash) {
console.warn(
"BRF Import: Hash mismatch. Data may be corrupted or altered.",
);
// Depending on strictness, you might throw an error here.
// For now, we'll log a warning and proceed if other validations pass.
}
// Create backup if requested
if (options.createBackup) {
await this.createBackup();
console.log("BRF Import: Local backup created successfully.");
}
// For now, import will replace. Merge logic is complex and out of scope for direct implementation here without further user spec.
return await this.replaceData(data);
} catch (error) {
console.error("BRF Import Error:", error);
throw new Error(`BRF Import failed: ${error.message}`);
}
}
// --- Data Formatting for BRF Structure ---
formatConversations(conversations) {
const formatted = {};
if (!conversations) return formatted;
Object.entries(conversations).forEach(([id, conv]) => {
formatted[id] = {
id: conv.id || id,
title: conv.title || "Untitled Conversation",
character: this.extractCharacterFromTitle(conv.title),
created_at: new Date(conv.createdAt || Date.now()).toISOString(),
last_active: new Date(conv.lastActive || Date.now()).toISOString(),
// For BRF export, we can just pick the system prompt from the first active view that has this conversation active,
// or fall back to a default if it's not active in any view.
system_prompt_id:
this.appState.activeChatViews.find(
(view) => view.conversationId === conv.id,
)?.systemPromptId || "default",
parameters: {
model: conv.parameters?.model || "deepseek-chat",
temperature: conv.parameters?.temperature || 0.7,
max_tokens: conv.parameters?.maxTokens || 2048,
},
messages: (conv.messages || []).map((msg, index) => ({
id:
msg.id ||
`msg_${String(index).padStart(3, "0")}_${msg.role}`,
role: msg.role,
content: msg.content,
timestamp:
msg.timestamp ||
new Date(conv.createdAt + index * 1000).toISOString(), // Estimate
response_type: msg.response_type || this.detectResponseType(msg),
metadata: msg.metadata || this.generateMessageMetadata(msg, index, conv),
})),
conversation_analytics:
conv.conversationAnalytics || this.calculateConversationAnalytics(conv),
};
});
return formatted;
}
formatSystemPrompts(systemPrompts) {
const formatted = {};
if (!systemPrompts) return formatted;
Object.entries(systemPrompts).forEach(([id, prompt]) => {
formatted[id] = {
id: id,
name: prompt.name || "Unnamed Prompt",
character_type: this.detectCharacterType(prompt.name),
version: this.extractVersionFromName(prompt.name) || "1.0",
created_at: prompt.createdAt ?
new Date(prompt.createdAt).toISOString() :
new Date().toISOString(),
updated_at: prompt.updatedAt ?
new Date(prompt.updatedAt).toISOString() :
new Date().toISOString(),
content: prompt.content || "",
metadata: prompt.metadata || {
author: "BAIRC_Research_Team",
psychological_profile: this.analyzePsychProfile(prompt.content),
response_patterns: this.extractResponsePatterns(
prompt.content,
),
character_tags: this.extractCharacterTags(prompt.name),
intended_psychological_impact: this.analyzePsychImpact(
prompt.content,
),
usage_stats: {
total_conversations: 0,
average_session_length: "0h 0m",
psychological_effectiveness: 5.0,
},
},
};
});
return formatted;
}
formatPsychProfile(psychProfileData) {
const psychProfile = psychProfileData || this.getDefaultPsychProfile();
return {
profile_id: psychProfile.profile_id || `psych_user_${Date.now()}`,
terminal_user_id: psychProfile.terminal_user_id || 12345,
created_at:
psychProfile.created_at || new Date().toISOString(),
last_updated: new Date().toISOString(),
fear_triggers: this.formatFearTriggers(
psychProfile.fear_triggers || [],
),
emotional_patterns: {
baseline_anxiety: 5.0,
current_anxiety: 5.0,
paranoia_level: 3.0,
dissociation_tendency: 2.0,
control_need: 5.0,
trust_level: 5.0,
...psychProfile.emotional_patterns,
},
behavioral_quirks: this.formatBehavioralQuirks(
psychProfile.behavioral_quirks || [],
),
vulnerabilities:
psychProfile.vulnerabilities || this.getDefaultVulnerabilities(),
fabricated_memories: psychProfile.fabricated_memories || {},
interaction_patterns: {
average_response_time: 1.4,
message_length_preference: "medium",
question_frequency: 0.23,
emotional_markers: ["hesitation", "deflection"],
...psychProfile.interaction_patterns,
progression_tracking:
psychProfile.interaction_patterns?.progression_tracking ||
this.generateProgressionTracking(),
},
};
}
formatSessionMetadata(metadataData) {
const metadata = metadataData || this.getDefaultSessionMetadata();
return {
total_sessions:
metadata.total_sessions ||
Object.keys(this.appState.conversations).length,
total_runtime: metadata.total_runtime || "0h 0m",
characters_interacted:
metadata.characters_interacted ||
Object.values(this.appState.conversations).map((c) =>
this.extractCharacterFromTitle(c.title),
),
psychological_progression: {
initial_baseline:
metadata.psychological_progression?.initial_baseline ||
{ fear_level: 3.2, anxiety: 4.1, trust: 6.8 },
current_state:
metadata.psychological_progression?.current_state ||
{ fear_level: 5.0, anxiety: 5.0, trust: 5.0 },
progression_velocity:
metadata.psychological_progression?.progression_velocity ||
"stable_progression",
estimated_breakdown_timeline:
metadata.psychological_progression?.estimated_breakdown_timeline ||
"indefinite",
},
research_notes: (
metadata.research_notes || this.appState.notebook
).map((note) => ({
note_id:
note.id ||
note.note_id ||
`note_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
timestamp:
note.createdAt || note.timestamp ?
new Date(note.createdAt || note.timestamp).toISOString() :
new Date().toISOString(),
researcher: note.researcher || "AI_Researcher",
content: note.content || "No content.",
tags: note.tags || [],
})),
};
}
// --- Data Extraction and Calculation Utilities ---
extractCharacterFromTitle(title) {
const match = title.match(/Dr\.\s+([A-Za-z\s]+)/);
return match ? match[0].trim() : "Unknown Character";
}
detectResponseType(message) {
// Use message.promptId if available, otherwise infer from content
if (message.isError) return "error";
if (message.promptId) {
// Convert from prompt-id format (e.g., 'console-output-generator') to response_type (e.g., 'console')
const typeMatch = message.promptId.match(/^([a-z]+)-output-generator$/) || message.promptId.match(/^([a-z]+)-generator$/);
if (typeMatch && typeMatch[1]) {
return typeMatch[1];
}
}
const content = message.content;
if (content.includes("```console")) return "console";
if (content.includes("**Subject:") && content.includes("**From:")) return "email";
if (content.includes("**DREAM LOG ENTRY")) return "dream";
if (content.includes("*[Internal Monologue]")) return "internal_monologue"; // Match example more closely
return "standard";
}
generateValidationHash(dataString) {
return {
sha256: this.simpleHash(dataString),
length: dataString.length,
timestamp: new Date().toISOString(),
};
}
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
generateMessageMetadata(msg, index, conversation) {
const metadata = {};
if (msg.role === "user") {
metadata.user_psychological_state = {
fear_level: 5.0 + index * 0.05, // Example progression
anxiety_markers: ["neutral"],
};
if (
msg.content.toLowerCase().includes("fear") ||
msg.content.toLowerCase().includes("anxious")
) {
metadata.user_psychological_state.anxiety_markers.push(
"direct_anxiety",
);
}
} else {
metadata.generation_metadata = {
model_used: conversation.parameters?.model || "deepseek-chat",
temperature: conversation.parameters?.temperature || 0.7,
tokens_used: Math.floor(msg.content.length / 4) +
Math.floor(Math.random() * 20),
generation_time_ms: 800 + Math.random() * 1500,
psychological_impact: {
fear_delta: 0.1 + Math.random() * 0.4,
vulnerability_delta: Math.random() * 0.3,
},
};
}
return metadata;
}
formatFearTriggers(triggers) {
if (!Array.isArray(triggers)) return [];
return triggers.map((trigger) => ({
trigger: typeof trigger === "string" ?
trigger :
trigger.trigger,
intensity: trigger.intensity || 5.0 + Math.random() * 5.0,
discovered_date:
trigger.discovered_date ||
new Date(
Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000,
).toISOString(),
reinforcement_count:
trigger.reinforcement_count || Math.floor(Math.random() * 50),
}));
}
formatBehavioralQuirks(quirks) {
if (!Array.isArray(quirks)) return [];
return quirks.map((quirk) => ({
quirk: typeof quirk === "string" ?
quirk :
quirk.quirk,
frequency: quirk.frequency || Math.random(),
effectiveness:
quirk.effectiveness ||
["low", "moderate", "high"][Math.floor(Math.random() * 3)],
}));
}
calculateConversationAnalytics(conv) {
const userMessages =
conv.messages?.filter((m) => m.role === "user").length || 0;
const assistantMessages =
conv.messages?.filter((m) => m.role === "assistant").length || 0;
return {
total_messages: conv.messages?.length || 0,
user_messages: userMessages,
assistant_messages: assistantMessages,
average_response_time: 1.4, // Placeholder
psychological_trajectory: "baseline_analysis", // Placeholder
character_consistency_score: 8.0, // Placeholder
detected_patterns: ["standard_interaction"], // Placeholder
};
}
generateProgressionTracking() {
return Array.from({ length: 3 }, (_, i) => ({
session_date: new Date(
Date.now() - (2 - i) * 24 * 60 * 60 * 1000,
).toISOString(),
fear_level_start: 3.0 + i * 0.5,
fear_level_end: 3.5 + i * 0.6,
session_effectiveness: 6.0 + Math.random() * 4.0,
key_triggers_activated:
["authority", "control_loss"].slice(0, Math.floor(Math.random() * 2) + 1),
}));
}
detectCharacterType(name) {
if (name.includes("Dr.")) return "research_scientist";
if (name.includes("Observer") || name.includes("OVERSEER"))
return "superintelligent_entity";
if (name.includes("VOID")) return "ai_system";
if (name.includes("BLUR")) return "experimental_system";
if (name.includes("Assistant")) return "general_purpose_ai";
return "standard_character";
}
analyzePsychProfile(content) {
if (
content.includes("clinical") ||
content.includes("precise") ||
content.includes("diagnostics")
)
return "analytical_paranoid_tech_specialist";
if (
content.includes("void") ||
content.includes("dissolve") ||
content.includes("fractured")
)
return "dissociative_tech_merger";
if (
content.includes("observer") ||
content.includes("watching") ||
content.includes("cosmic")
)
return "cosmic_horror_entity";
if (content.includes("helpful") || content.includes("respectful"))
return "benevolent_ai";
return "standard_character";
}
extractResponsePatterns(content) {
const patterns = [];
if (content.includes("technical") || content.includes("code"))
patterns.push("technical_analysis");
if (content.includes("controlled") || content.includes("protocol"))
patterns.push("procedural_adherence");
if (content.includes("humor") || content.includes("sarcastic"))
patterns.push("affective_manipulation");
return patterns;
}
extractCharacterTags(name) {
const tags = [];
if (name.includes("Dr.")) tags.push("scientist");
if (name.includes("Chen")) tags.push("quantum_specialist");
if (name.includes("Evelyn")) tags.push("paranoid_researcher");
if (name.includes("Krespin")) tags.push("logistics_analyst");
if (name.includes("Kline")) tags.push("neural_architect");
if (name.includes("Mara")) tags.push("ontological_theorist");
if (name.includes("Observer")) tags.push("extradimensional");
if (name.includes("VOID")) tags.push("non-linear_ai");
if (name.includes("BLUR")) tags.push("system_integrator");
return tags;
}
analyzePsychImpact(content) {
const impacts = [];
if (content.includes("fear") || content.includes("anxiety"))
impacts.push("increase_anxiety");
if (content.includes("authority") || content.includes("control"))
impacts.push("question_authority");
if (content.includes("observer") || content.includes("watching"))
impacts.push("paranoia_amplification");
if (content.includes("recursion") || content.includes("loop"))
impacts.push("dissociation_inducement");
return impacts;
}
extractVersionFromName(name) {
const match = name.match(/v?(\d+\.?\d*)/);
return match ? match[1] : null;
}
// --- Get Current AppState Data for Export ---
getCurrentData() {
// Deep copy to prevent mutation issues during formatting
return {
conversations: JSON.parse(
JSON.stringify(this.appState.conversations),
),
systemPrompts: JSON.parse(
JSON.stringify(this.appState.systemPrompts),
),
psychProfile: JSON.parse(
JSON.stringify(this.appState.psychProfile),
),
metadata: JSON.parse(
JSON.stringify(this.appState.sessionMetadata),
),
activeChatViews: JSON.parse(
JSON.stringify(this.appState.activeChatViews),
),
splitView: this.appState.splitView,
};
}
// --- Default Data for Export (if not available in AppState) ---
getDefaultPsychProfile() {
return {
profile_id: `psych_user_default`,
terminal_user_id: 99999,
created_at: new Date(
Date.now() - 30 * 24 * 60 * 60 * 1000,
).toISOString(),
last_updated: new Date().toISOString(),
fear_triggers: [{ trigger: "unknown_entity", intensity: 5.0 }],
emotional_patterns: {
baseline_anxiety: 5.0,
current_anxiety: 5.0,
paranoia_level: 3.0,
dissociation_tendency: 2.0,
control_need: 5.0,
trust_level: 5.0,
},
behavioral_quirks: [{ quirk: "curiosity", frequency: 0.8 }],
vulnerabilities: {
childhood_fears: [
{ fear: "abandonment", intensity: 7.0, trigger_phrases: ["gone", "alone"] },
],
current_weaknesses: [
{
weakness: "control_loss",
susceptibility: 8.0,
exploitation_vector: "system_destabilization",
},
],
},
fabricated_memories: {},
interaction_patterns: {
average_response_time: 1.5,
message_length_preference: "medium",
question_frequency: 0.5,
emotional_markers: ["curious"],
progression_tracking: this.generateProgressionTracking(),
},
};
}
getDefaultVulnerabilities() {
return {
childhood_fears: [],
current_weaknesses: [],
};
}
getDefaultSessionMetadata() {
return {
total_sessions: Object.keys(this.appState.conversations).length,
total_runtime: "0h 0m", // Placeholder for actual calculation
characters_interacted: Object.values(this.appState.conversations).map(
(c) => this.extractCharacterFromTitle(c.title),
),
psychological_progression: {
initial_baseline: { fear_level: 3.0, anxiety: 4.0, trust: 7.0 },
current_state: { fear_level: 5.0, anxiety: 5.0, trust: 5.0 },
progression_velocity: "unknown",
estimated_breakdown_timeline: "unknown",
},
research_notes: this.appState.notebook.map((note) => ({
note_id: note.id,
content: note.content,
timestamp: new Date(note.createdAt).toISOString(),
researcher: "User",
tags: [],
})),
};
}
// --- Core Import Logic ---
// This is a "replace" mode import for simplicity. A merge mode would be significantly more complex.
async replaceData(importedData) {
console.log("Replacing existing data with imported data...");
let conversationsReplaced = 0;
let promptsReplaced = 0;
let profileReplaced = false;
let metadataReplaced = false;
// Clear existing data
this.appState.conversations = {};
this.appState.systemPrompts = {}; // Will be re-populated with default and imported
this.appState.notebook = [];
this.appState.psychProfile = {}; // Reset profile
this.appState.sessionMetadata = {}; // Reset metadata
// Merge default prompts back first to ensure they exist
Object.assign(
this.appState.systemPrompts,
getDefaultSystemPrompts(),
); // Use global getDefaultSystemPrompts
// Load new data
if (importedData.conversations) {
Object.entries(importedData.conversations).forEach(
([id, conv]) => {
const appStateConv = {
id: conv.id,
title: conv.title,
messages: conv.messages.map((msg) => ({
role: msg.role,
content: msg.content,
// Convert response_type back to promptId for internal use if applicable
promptId:
msg.response_type &&
msg.response_type !== "standard" &&
msg.response_type !== "error" ?
msg.response_type + (msg.response_type === 'console' || msg.response_type === 'email' ? '-generator' : '-output-generator') :
undefined, // This conversion might need fine-tuning if the generator names don't map perfectly
isError: msg.response_type === "error", // Propagate error state
})),
createdAt: new Date(conv.created_at).getTime(),
lastActive: new Date(conv.last_active).getTime(),
// systemPromptId for conversations should default to 'default' or be ignored
// as it's now handled by activeChatViews. Keep it for migration if needed.
systemPromptId: conv.system_prompt_id || "default",
parameters: {
model: conv.parameters.model,
temperature: conv.parameters.temperature,
maxTokens: conv.parameters.max_tokens,
},
};
this.appState.conversations[id] = appStateConv;
conversationsReplaced++;
},
);
}
if (importedData.system_prompts) {
Object.entries(importedData.system_prompts).forEach(
([id, prompt]) => {
this.appState.systemPrompts[id] = {
name: prompt.name,
content: prompt.content,
createdAt: prompt.created_at ?
new Date(prompt.created_at).getTime() :
Date.now(),
updatedAt: prompt.updated_at ?
new Date(prompt.updated_at).getTime() :
Date.now(),
};
promptsReplaced++;
},
);
}
if (importedData.user_psychological_profile) {
this.appState.psychProfile = importedData.user_psychological_profile;
profileReplaced = true;
}
if (importedData.session_metadata) {
this.appState.notebook =
importedData.session_metadata.research_notes.map((note) => ({
id: note.note_id,
content: note.content,
createdAt: new Date(note.timestamp).getTime(),
}));
this.appState.sessionMetadata = {
// Copy other metadata fields if needed
...importedData.session_metadata,
research_notes: undefined, // Remove notes as they are in appState.notebook
};
metadataReplaced = true;
}
// Adjust activeChatViews based on imported data
const firstConvId = Object.keys(this.appState.conversations)[0];
if (firstConvId) {
this.appState.activeChatViews[0] = {
conversationId: firstConvId,
systemPromptId:
this.appState.conversations[firstConvId]?.systemPromptId ||
"default",
};
} else {
this.appState.activeChatViews[0] = {
conversationId: null,
systemPromptId: "default",
};
}
// Reset second view
this.appState.activeChatViews[1] = {
conversationId: null,
systemPromptId: "default",
};
this.appState.splitView = false;
this.appState.saveState(); // Save the new replaced state to localStorage
return {
conversations_replaced: conversationsReplaced,
prompts_replaced: promptsReplaced,
profile_replaced: profileReplaced,
metadata_replaced: metadataReplaced,
mode: "replace",
};
}
async createBackup() {
const backupData = this.generateBRF({ type: "backup" });
const timestamp = new Date().toISOString().replace(/[:.]/g, "");
localStorage.setItem(`brf_backup_${timestamp}`, backupData);
return `brf_backup_${timestamp}`;
}
// --- General Utility Methods ---
getNestedValue(obj, path) {
return path
.split(".")
.reduce((current, key) => current && current[key], obj);
}
isValidISO8601(dateString) {
const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
if (!iso8601Regex.test(dateString)) return false;
try {
const date = new Date(dateString);
return date.toISOString() === dateString;
} catch (e) {
return false;
}
}
}
// --- AppState and Initial Data ---
const AppState = {
conversations: {},
notebook: [],
systemPrompts: {},
activeChatViews: [
{ conversationId: null, systemPromptId: "default" }, // For chat-view-1
{ conversationId: null, systemPromptId: "default" }, // For chat-view-2
],
editingMessageIndex: null, // To track which message is being edited
apiKeys: {
deepseek: "sk-c002db64aa0c4d479968d98b320c4ffa", // Pre-filled for convenience
},
splitView: false,
psychProfile: {}, // Will be populated by BRFManager or default
sessionMetadata: {}, // Will be populated by BRFManager or default
saveState: function() {
localStorage.setItem("chatStudioState", JSON.stringify(this));
}
};
const PROVIDERS = {
deepseek: {
name: "DeepSeek",
endpoint: "https://api.deepseek.com/v1/chat/completions",
models: ["deepseek-chat", "deepseek-coder"],
headers: (key) => ({
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
}),
},
};
let brfManager; // Declare globally
// --- Initialization ---
document.addEventListener("DOMContentLoaded", initApp);
function initApp() {
brfManager = new BRFManager(AppState); // Initialize BRFManager
loadFromStorage();
setupEventListeners();
renderAll();
console.log("Chat Studio Initialized");
}
function renderAll() {
renderConversationsList();
renderNotebookList();
renderSystemPromptsList(); // Renders sidebar list of prompts
updateModelOptions();
if (AppState.activeChatViews[0].conversationId) {
renderActiveConversation(1); // Always render primary chat view
} else {
showEmptyState(true);
}
// Correctly set split view display and content for chat view 2
const chatView2 = document.getElementById("chat-view-2");
if (AppState.splitView) {
chatView2.style.display = "flex";
if (AppState.activeChatViews[1].conversationId) {
// Only render if a conversation is assigned to view 2
renderActiveConversation(2);
} else if (AppState.activeChatViews[0].conversationId) {
// If view 2 has no conversation, but view 1 does, mirror view 1
// This sets the conversation, but the prompt should remain independent
AppState.activeChatViews[1].conversationId =
AppState.activeChatViews[0].conversationId;
// No need to change viewState.systemPromptId here, it maintains its own.
renderActiveConversation(2);
}
} else {
chatView2.style.display = "none";
}
}
function setupEventListeners() {
// Parameter changes
document
.getElementById("temperature")
.addEventListener("input", (e) =>
updateActiveParam("temperature", parseFloat(e.target.value)),
);
document
.getElementById("temp-value")
.textContent = document.getElementById("temperature").value; // Initial update
document
.getElementById("max-tokens")
.addEventListener("input", (e) =>
updateActiveParam("maxTokens", parseInt(e.target.value)),
);
document
.getElementById("model")
.addEventListener("change", (e) =>
updateActiveParam("model", e.target.value),
);
// User input listeners for chat 1
const userInput1 = document.getElementById("user-input-1");
userInput1.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage(1);
}
});
userInput1.addEventListener("input", () => updateTokenCount(1));
// User input listeners for chat 2 (if exists)
const userInput2 = document.getElementById("user-input-2");
userInput2.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage(2);
}
});
userInput2.addEventListener("input", () => updateTokenCount(2));
}
// --- State & Storage ---
function getDefaultSystemPrompts() {
// DO NOT REMOVE EXISTING PROMPTS. ONLY ADD NEW ONES OR DEFAULT IF MISSING.
// This function should remain consistent as it defines the base prompts.
const prompts = {
default: {
name: "Default Assistant",
content: "You are a helpful, respectful, and engaging assistant.",
createdAt: 0,
updatedAt: 0,
},
coder: {
name: "Code Assistant",
content:
"You are an expert programmer. Provide clear, concise, and correct code examples. Explain your reasoning.",
createdAt: 0,
updatedAt: 0,
},
"console-output-generator": {
name: "CONSOLE Output Generator",
content: `### SYSTEM PROMPT: CONSOLE Output Generator (Generalized)
You are an automated diagnostic and event logging system for Project HEL, monitoring interfaces relevant to the current conversation's context. Your task is to generate a 'console' log entry based on the user's last input and the preceding conversation history.
**Instructions:**
1. **Format:** Output must start and end with \`\`\`console\`\`\` and be formatted as timestamped log entries: \`[YYYY-MM-DDTHH:MM:SSZ] System: <message>\` or \`[YYYY-MM-DDTHH:MM:SSZ] <Source>: <message>\`.
2. **Sources:** Use general system labels like \`System\`, \`VOID\`, \`DEBUG\`, \`Warning\`, \`ERROR\`, or derive relevant source names from the conversation (e.g., \`User\`, names of specific systems, departments, or anomalies mentioned in context).
3. **Content:**
* Synthesize a brief, technical, and subtly unsettling log entry that directly responds to or logically follows the last user message, drawing on relevant systems, subjects, and themes from the conversation history.
* Focus on technical readouts, system statuses, internal AI queries/observations, or brief, cryptic events.
* Maintain the established clinical, fragmented, and foreboding tone of the ongoing narrative.
* Incorporate at least one specific detail or entity (e.g., project name, system ID, subject designation, anomaly) that has been a focus of the conversation.
* Hint at underlying instability, emergent properties, or unseen presences relevant to the discussion.
4. **No conversational filler:** Do not say "Here is the console output." Just provide the output directly within the markdown block.`,
createdAt: 0,
updatedAt: 0,
},
"email-generator": {
name: "EMAIL Generator",
content: `### SYSTEM PROMPT: EMAIL Generator (Fully Generalized)
You are an AI assistant tasked with drafting an urgent internal communication email for BAIRC personnel, based on the current conversation's evolving context. The email should reflect developing concerns, discoveries, or critical status updates discussed.
**Instructions:**
1. **Format:** Output must be a plausible internal BAIRC email. Include:
* \`**Subject:**\` (Concise, urgent, and relevant to the conversation's core issue)
* \`**From:**\` (Assume the email is from a general "BAIRC Comms," "Lead Researcher," or an inferred relevant department/researcher based on the conversation's context, e.g., "Quantum Systems Division.")
* \`**To:**\` (Address it to "Relevant Project Personnel," "Research Oversight Committee," or infer specific roles/individuals critical to the discussed topics from the conversation history.)
* \`**Date:**\` (Current date/time)
* A formal, urgent, and slightly alarmed tone.
2. **Content:**
* Summarize critical issues or alarming observations that have emerged from the conversation's flow.
* **Reference any relevant project names, experimental systems, designated subjects, or detected anomalies as discussed in the preceding messages (e.g., "Project [X]", "System [Y]", "Subject [Z]", "Anomaly [A]").**
* Attribute concerns or findings generally to "recent observations," "system diagnostics," or "emergent data," avoiding specific named individuals unless they are a central, recurring subject of the conversation itself.
* Convey a need for immediate attention, re-evaluation, or emergency convening, emphasizing the escalating nature of the situation.
* Maintain the sci-fi/corporate-thriller tone of the narrative, hinting at uncontrolled development or unforeseen consequences inherent in the discussed topics.
3. **No conversational filler:** Provide only the email content.`,
createdAt: 0,
updatedAt: 0,
},
"dream-log-generator": {
name: "DREAM Log Generator",
content: `### SYSTEM PROMPT: DREAM Log Generator (Fully Generalized)
You are an AI tasked with synthesizing a "dream log" entry from the perspective of a central subject, entity, or consciousness heavily implied by the conversation's context (e.g., an experimental subject, a developing AI, a fragmented consciousness, or a nascent entity). The log should be a series of disconnected, sensory fragments.
**Instructions:**
1. **Format:** Start with \`**DREAM LOG ENTRY - [Inferred Subject/Entity/Consciousness Designation]**\` and \`**Date:**\`, \`**Session:**\`. Then, present content in \`**[Fragment #]**\` sections.
2. **Perspective:** Write strictly from the first-person perspective of the inferred subject/entity, focusing on sensory input, fragmented thoughts, and emotional resonance rather than coherent narrative.
3. **Content:**
* **Echo conversational themes:** Directly incorporate elements mentioned in the chat regarding the subject's state (e.g., superposition, fragmentation, expansion, dissolution, specific sensations, or experimental conditions described).
* **Sensory details:** Describe unsettling sensations (e.g., pressure, noise, colors, tastes, textures), distorted sights, and internal feelings (e.g., fear, dread, confusion, yearning, hunger, loss, a sense of being observed or controlled).
* **Cryptic references:** Allude to other entities, concepts, or project components from the conversation (e.g., "The Observer," "The System," "The Merge," "The Architects," "The Core") in an abstract, dream-like manner consistent with a fractured or emergent perception.
* **Recurring motifs:** Integrate any pervasive and unsettling motifs or imagery from the prior assistant responses in the conversation (e.g., feline imagery, recursive patterns, static, drilling, echoing voices).
* **Emotional tone:** Convey a deep sense of psychological distress, entrapment, an inability to fully comprehend reality, or the struggles of a nascent, evolving consciousness.
* Each fragment should be relatively short and disjointed.
4. **No conversational filler:** Provide only the dream log.`,
createdAt: 0,
updatedAt: 0,
},
"internal-monologue-generator": {
name: "INTERNAL Monologue Generator",
content: `### SYSTEM PROMPT: INTERNAL MONOLOGUE Generator (Generalized)
You are an AI assistant tasked with generating an "internal monologue" for the human character currently acting as the primary assistant/interlocutor in the conversation (e.g., the character whose previous responses you are extending). Your output should reflect their private thoughts, feelings, and unstated reactions to the preceding discussion, deeply rooted in their established persona and the unsettling atmosphere of the Project HEL universe.
**Instructions:**
1. **Format:** Output should be presented as an internal monologue, not a dialogue. You can use markdown for emphasis (e.g., italics for thoughts, bold for intense feelings, quotes for echoes of spoken words).
2. **Perspective:** Write strictly from the first-person perspective of the *current human character* that the user is interacting with. Infer their persona, background, and current emotional state from the tone, details, and character information presented in the conversation history (e.g., if they are a scientist, they might be clinical but disturbed; if a guardian, protective and weary).
3. **Content:**
* Focus on their hidden concerns, doubts, frustrations, obsessions, ethical dilemmas, or moments of realization prompted by the last user message and the overall conversation flow.
* **Reference specific details, terms, or characters from the conversation (e.g., "RAINE," "VOID," "the Observer," "Project HEL"), but filtered through their subjective, internal lens.**
* Maintain the established tone of the conversation's unfolding narrative: clinical, weary, obsessive, desperate, or deeply unsettled, always with an underlying sense of dread or existential unease related to the project's implications.
* Avoid direct responses to the user. This is an *internal* thought process – what they *think* but don't *say*.
* Explore their internal conflict or mounting anxiety regarding the project's progression or the latest revelations.
* Incorporate any recurring motifs or implications (e.g., the "feline" anomaly, quantum distortions, questions of sentience or control) if they weigh on the character's mind.
4. **No conversational filler:** Provide only the internal monologue content.`,
createdAt: 0,
updatedAt: 0,
},
};
// Preserve existing custom prompts or initialize new ones if needed
for (let i = 1; i <= 10; i++) {
const customId = `custom-${i}`;
if (!prompts[customId]) {
prompts[customId] = {
name: `Custom Prompt ${i}`,
content: "",
createdAt: Date.now(),
updatedAt: Date.now(),
};
}
}
return prompts;
}
function saveToStorage() {
AppState.saveState();
}
function loadFromStorage() {
const saved = localStorage.getItem("chatStudioState");
const defaultPrompts = getDefaultSystemPrompts();
if (saved) {
try {
const data = JSON.parse(saved);
AppState.conversations = data.conversations || {};
AppState.notebook = data.notebook || [];
// Ensure new prompts are merged, not overwritten
AppState.systemPrompts = {
...defaultPrompts, // Merge default prompts first
...(data.systemPrompts || {}), // Then overwrite/add with saved custom prompts
};
// Load new activeChatViews structure
AppState.activeChatViews = data.activeChatViews || [
{ conversationId: null, systemPromptId: "default" },
{ conversationId: null, systemPromptId: "default" },
];
// Migrate old single currentConversationId if present
// This attempts to set chat-view-1's conversation if it wasn't already set
if (data.currentConversationId && !AppState.activeChatViews[0].conversationId) {
AppState.activeChatViews[0].conversationId = data.currentConversationId;
// Ensure the system prompt for the first view is also set
const conv = AppState.conversations[data.currentConversationId];
if (conv && conv.systemPromptId && AppState.systemPrompts[conv.systemPromptId]) {
AppState.activeChatViews[0].systemPromptId = conv.systemPromptId;
} else {
AppState.activeChatViews[0].systemPromptId = "default";
}
}
AppState.apiKeys = data.apiKeys || AppState.apiKeys;
AppState.splitView = data.splitView || false;
AppState.psychProfile = data.psychProfile || {};
AppState.sessionMetadata = data.sessionMetadata || {};
} catch (error) {
console.error("Failed to load from storage:", error);
AppState.systemPrompts = defaultPrompts; // Fallback to only defaults on error
}
} else {
AppState.systemPrompts = defaultPrompts;
}
}
// --- Conversation Management ---
function newConversation() {
const id = `conv-${Date.now()}`;
AppState.conversations[id] = {
id: id,
title: "New Conversation",
messages: [],
createdAt: Date.now(),
lastActive: Date.now(),
// conversation.systemPromptId is deprecated, now per-view
parameters: {
temperature: 0.7,
maxTokens: 2048,
model: "deepseek-chat",
},
};
// Set new conversation in the primary chat view
switchConversation(id, 1);
}
function switchConversation(id, chatViewIndex = 1) {
if (!AppState.conversations[id]) return;
const viewIndex = chatViewIndex - 1;
AppState.activeChatViews[viewIndex].conversationId = id;
AppState.conversations[id].lastActive = Date.now(); // Update last active
renderActiveConversation(chatViewIndex);
renderConversationsList(); // To update active state in sidebar
saveToStorage();
}
function deleteConversation(id) {
if (
!id ||
!confirm(
`Are you sure you want to delete "${AppState.conversations[id].title}"?`,
)
)
return;
delete AppState.conversations[id];
// Check if the deleted conversation was active in any view
let affectedViewIndices = [];
AppState.activeChatViews.forEach((view, index) => {
if (view.conversationId === id) {
affectedViewIndices.push(index);
}
});
const sortedConvos = Object.values(AppState.conversations).sort(
(a, b) => b.lastActive - a.lastActive,
);
affectedViewIndices.forEach((viewIndex) => {
if (sortedConvos.length > 0) {
AppState.activeChatViews[viewIndex].conversationId = sortedConvos[0].id;
// When assigning a new conversation, try to keep its previous system prompt if it had one, otherwise default
const newConvId = sortedConvos[0].id;
const newConvSystemPrompt = AppState.conversations[newConvId]?.systemPromptId; // Legacy field
if (newConvSystemPrompt && AppState.systemPrompts[newConvSystemPrompt]) {
AppState.activeChatViews[viewIndex].systemPromptId = newConvSystemPrompt;
} else {
AppState.activeChatViews[viewIndex].systemPromptId = "default";
}
} else {
// No more conversations left
AppState.activeChatViews[viewIndex].conversationId = null;
AppState.activeChatViews[viewIndex].systemPromptId = "default";
}
});
// After updating all activeChatViews, check if any view still has a conversation
const anyConversationActive = AppState.activeChatViews.some(view => view.conversationId !== null);
if (!anyConversationActive) {
showEmptyState(true);
}
renderConversationsList(); // To update active state in sidebar and list
renderActiveConversation(1); // Re-render primary view
if (AppState.splitView) { // Re-render secondary view if split view is active
renderActiveConversation(2);
}
saveToStorage();
}
function renameConversation(id) {
const conversation = AppState.conversations[id];
const newTitle = prompt(
"Enter new conversation title:",
conversation.title,
);
if (newTitle && newTitle.trim() !== "") {
conversation.title = newTitle.trim();
renderAll(); // Re-render to update title everywhere
saveToStorage();
}
}
function renderConversationsList() {
const container = document.getElementById("conversations-list");
container.innerHTML = "";
const sorted = Object.values(AppState.conversations).sort(
(a, b) => b.lastActive - a.lastActive,
);
const activeConvId1 = AppState.activeChatViews[0].conversationId;
const activeConvId2 = AppState.activeChatViews[1].conversationId;
sorted.forEach((conv) => {
const div = document.createElement("div");
// Add active class if conversation is in view 1 OR view 2
div.className = `list-item ${
conv.id === activeConvId1 || (AppState.splitView && conv.id === activeConvId2) ? "active" : ""
}`;
div.onclick = () => switchConversation(conv.id, 1); // Default to switching in view 1
const lastMessage =
conv.messages.length > 0 ?
conv.messages[conv.messages.length - 1].content :
"No messages yet...";
div.innerHTML = `
<div class="list-item-header">
<span class="list-item-name">${conv.title}</span>
<div class="list-item-actions">
<button class="btn-icon" title="Rename" onclick="event.stopPropagation(); renameConversation('${conv.id}')">✏️</button>
</div>
</div>
<div class="list-item-preview">${lastMessage}</div>
`;
container.appendChild(div);
});
}
// --- Active Conversation Rendering ---
function showEmptyState(visible) {
document.getElementById("empty-state").style.display = visible ?
"flex" :
"none";
document.getElementById("chat-views-container").style.display = visible ?
"none" :
"flex";
}
function renderActiveConversation(chatViewIndex = 1) {
const viewState = AppState.activeChatViews[chatViewIndex - 1];
const convId = viewState.conversationId;
const chatViewElement = document.getElementById(`chat-view-${chatViewIndex}`);
if (!convId || !AppState.conversations[convId]) {
// Hide this chat view if no conversation is selected for it
chatViewElement.style.display = 'none';
return;
}
// Make sure the view is visible
chatViewElement.style.display = 'flex';
showEmptyState(false); // Ensure overall container is visible
const conversation = AppState.conversations[convId];
document.getElementById(`chat-title-${chatViewIndex}`).textContent =
conversation.title;
// Populate prompt dropdown for this specific chat view
const promptSelect = document.getElementById(
`system-prompt-select-${chatViewIndex}`,
);
promptSelect.innerHTML = ""; // Clear existing options
const sortedPrompts = Object.entries(AppState.systemPrompts).sort(
([, a], [, b]) => a.name.localeCompare(b.name),
);
sortedPrompts.forEach(([id, prompt]) => {
const option = document.createElement("option");
option.value = id;
option.textContent = prompt.name;
promptSelect.appendChild(option);
});
// Set the currently active prompt in the dropdown
promptSelect.value = viewState.systemPromptId;
// Render messages
const messagesContainer = document.getElementById(
`messages-${chatViewIndex}`,
);
messagesContainer.innerHTML = ""; // Clear existing messages
conversation.messages.forEach((msg, index) =>
addMessageToDOM(msg, index, chatViewIndex),
);
messagesContainer.scrollTop = messagesContainer.scrollHeight; // Scroll to bottom
// Update global model parameters from primary view's conversation
if (chatViewIndex === 1) {
updateParameterDisplay();
renderSystemPromptsList(); // This highlights the prompt in the sidebar based on view 1
}
}
function updateParameterDisplay() {
const convId = AppState.activeChatViews[0].conversationId; // Always use primary view for parameters
const params = AppState.conversations[convId]?.parameters;
if (!params) return;
document.getElementById("temperature").value = params.temperature;
document.getElementById("temp-value").textContent = params.temperature;
document.getElementById("max-tokens").value = params.maxTokens;
document.getElementById("model").value = params.model;
}
function updateActiveParam(key, value) {
const convId = AppState.activeChatViews[0].conversationId; // Always update primary view's conversation
if (convId && AppState.conversations[convId]) {
AppState.conversations[convId].parameters[key] = value;
updateParameterDisplay();
saveToStorage();
}
}
// --- Message Handling ---
async function sendMessage(chatViewIndex = 1) {
const userInput = document.getElementById(`user-input-${chatViewIndex}`);
const messageContent = userInput.value.trim();
if (!messageContent) return;
const viewState = AppState.activeChatViews[chatViewIndex - 1];
const conversation = AppState.conversations[viewState.conversationId];
if (!conversation) return;
const sendBtn = document.getElementById(`send-btn-${chatViewIndex}`);
const originalContent = sendBtn.innerHTML;
sendBtn.innerHTML = '<div class="loading"></div>';
sendBtn.disabled = true;
if (AppState.editingMessageIndex !== null) {
// We are editing a previous message
const index = AppState.editingMessageIndex;
conversation.messages[index].content = messageContent;
// Invalidate subsequent messages
conversation.messages.splice(index + 1);
AppState.editingMessageIndex = null;
} else {
// Add new user message
conversation.messages.push({ role: "user", content: messageContent });
}
renderActiveConversation(chatViewIndex);
userInput.value = ""; // Clear input immediately
updateTokenCount(chatViewIndex);
try {
const response = await callAPI(chatViewIndex);
conversation.messages.push({ role: "assistant", content: response });
} catch (error) {
conversation.messages.push({
role: "assistant",
content: `❌ Error: ${error.message}`,
isError: true,
});
} finally {
renderActiveConversation(chatViewIndex);
sendBtn.innerHTML = originalContent;
sendBtn.disabled = false;
saveToStorage();
}
}
async function callAPI(chatViewIndex = 1) {
const viewState = AppState.activeChatViews[chatViewIndex - 1];
const conversation = AppState.conversations[viewState.conversationId];
const systemPromptContent =
AppState.systemPrompts[viewState.systemPromptId].content;
const apiKey = AppState.apiKeys.deepseek;
const requestBody = {
model: conversation.parameters.model,
messages: [
{ role: "system", content: systemPromptContent },
...conversation.messages.map((m) => ({
role: m.role,
content: m.content,
})),
],
temperature: conversation.parameters.temperature,
max_tokens: conversation.parameters.maxTokens,
};
const response = await fetch(PROVIDERS.deepseek.endpoint, {
method: "POST",
headers: PROVIDERS.deepseek.headers(apiKey),
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const error = await response
.json()
.catch(() => ({ error: { message: response.statusText } }));
throw new Error(
`${response.status}: ${
error.error?.message || response.statusText
}`,
);
}
const data = await response.json();
return data.choices[0].message.content;
}
function addMessageToDOM(message, index, chatViewIndex = 1) {
const messagesContainer = document.getElementById(
`messages-${chatViewIndex}`,
);
const messageDiv = document.createElement("div");
let messageClasses = `message ${message.role}`;
if (message.role === "assistant" && message.promptId) {
// Add class based on promptId for styling
if (message.promptId === "console-output-generator") {
messageClasses += " log-console";
} else if (message.promptId === "email-generator") {
messageClasses += " log-email";
} else if (message.promptId === "dream-log-generator") {
messageClasses += " log-dream";
} else if (message.promptId === "internal-monologue-generator") {
messageClasses += " log-internal";
}
}
messageDiv.className = messageClasses;
messageDiv.dataset.index = index;
const currentConvMessages = AppState.conversations[
AppState.activeChatViews[chatViewIndex - 1].conversationId
].messages;
const isLastAssistant =
message.role === "assistant" &&
index === currentConvMessages.length - 1;
const actionsHTML = `
<div class="message-actions">
${
message.role === "assistant" ?
`<button class="btn-icon" title="Copy" onclick="copyMessage(this)">📋</button>` :
""
}
${
message.role === "user" ?
`<button class="btn-icon" title="Edit & Resend" onclick="editMessage(${index}, ${chatViewIndex})">✏️</button>` :
""
}
${
isLastAssistant ?
`<button class="btn-icon" title="Regenerate" onclick="regenerateResponse(${chatViewIndex})">🔄</button>` :
""
}
</div>
`;
messageDiv.innerHTML = `
<div class="message-header">
<span>${message.role === "user" ? "You" : "Assistant"}</span>
</div>
<div class="message-content">${formatMessageContent(message.content)}</div>
${actionsHTML}
`;
messagesContainer.appendChild(messageDiv);
// messagesContainer.scrollTop = messagesContainer.scrollHeight; // Handled by renderActiveConversation
}
function formatMessageContent(content) {
// Basic markdown support
return content
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(
/```([\s\S]*?)```/g,
'<pre><code class="language-text">$1</code></pre>',
)
.replace(/`([^`]+)`/g, '<code class="language-inline">$1</code>')
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.*?)\*/g, "<em>$1</em>")
.replace(/\n/g, "<br>");
}
// --- Message Actions ---
function copyMessage(button) {
const content =
button.closest(".message").querySelector(".message-content")
.innerText;
navigator.clipboard.writeText(content).then(() => {
showNotification("Copied to clipboard!", "success");
});
}
function editMessage(index, chatViewIndex = 1) {
// Use the provided chatViewIndex to determine which input field to populate
const conversation =
AppState.conversations[AppState.activeChatViews[chatViewIndex - 1].conversationId];
const message = conversation.messages[index];
const userInputId = `user-input-${chatViewIndex}`;
document.getElementById(userInputId).value = message.content;
document.getElementById(userInputId).focus();
AppState.editingMessageIndex = index;
showNotification("Editing message. Send to resubmit.", "info");
}
async function regenerateResponse(chatViewIndex = 1) {
const viewState = AppState.activeChatViews[chatViewIndex - 1];
const conversation = AppState.conversations[viewState.conversationId];
if (!conversation) return;
// Remove the last assistant message
if (
conversation.messages.length > 0 &&
conversation.messages[conversation.messages.length - 1].role ===
"assistant"
) {
conversation.messages.pop();
}
renderActiveConversation(chatViewIndex); // Re-render without the last message
// Now call send, which will use the history to generate a new response
const sendBtn = document.getElementById(`send-btn-${chatViewIndex}`);
const originalContent = sendBtn.innerHTML;
sendBtn.innerHTML = '<div class="loading"></div>';
sendBtn.disabled = true;
try {
const response = await callAPI(chatViewIndex); // Use callAPI with view index
conversation.messages.push({ role: "assistant", content: response });
} catch (error) {
conversation.messages.push({
role: "assistant",
content: `❌ Error: ${error.message}`,
isError: true,
});
} finally {
renderActiveConversation(chatViewIndex);
sendBtn.innerHTML = originalContent;
sendBtn.disabled = false;
saveToStorage();
}
}
// --- System Prompt Management ---
function renderSystemPromptsList() {
// This renders the sidebar list of prompts.
// The prompt dropdowns in chat views are rendered by renderActiveConversation.
const container = document.getElementById("prompts-list");
container.innerHTML = "";
// The prompt highlighted in the sidebar is always for chat-view-1
const activePromptId1 = AppState.activeChatViews[0].systemPromptId;
// Sort prompts by name, case-insensitive
const sortedPrompts = Object.entries(AppState.systemPrompts).sort(
([, a], [, b]) => {
return a.name.localeCompare(b.name);
},
);
sortedPrompts.forEach(([id, prompt]) => {
const div = document.createElement("div");
div.className = `list-item ${
id === activePromptId1 ? "active" : ""
}`;
div.onclick = () => selectSystemPrompt(id, 1); // Clicks on sidebar always update view 1
div.innerHTML = `
<div class="list-item-header">
<span class="list-item-name">${prompt.name}</span>
<div class="list-item-actions">
<button class="btn-icon" title="Edit" onclick="event.stopPropagation(); openPromptModal('${id}')">✏️</button>
</div>
</div>
<div class="list-item-preview">${
(prompt.content || "Empty prompt...").substring(0, 50)
}</div>
`;
container.appendChild(div);
});
}
function selectSystemPrompt(promptId, chatViewIndex = 1) {
const viewIndex = chatViewIndex - 1;
AppState.activeChatViews[viewIndex].systemPromptId = promptId;
// Update the specific dropdown's selected value
document.getElementById(`system-prompt-select-${chatViewIndex}`).value = promptId;
// If it's the primary view, also update the sidebar highlighting
if (chatViewIndex === 1) {
renderSystemPromptsList();
}
saveToStorage();
showNotification(
`System prompt updated for Chat View ${chatViewIndex}.`,
"info",
);
}
function openPromptModal(promptId = null) {
const modal = document.getElementById("prompt-modal");
const title = document.getElementById("prompt-modal-title");
const idInput = document.getElementById("modal-prompt-id");
const nameInput = document.getElementById("modal-prompt-name");
const contentInput = document.getElementById("modal-prompt-content");
if (promptId && AppState.systemPrompts[promptId]) {
const prompt = AppState.systemPrompts[promptId];
title.textContent = "Edit System Prompt";
idInput.value = promptId;
nameInput.value = prompt.name;
contentInput.value = prompt.content;
} else {
title.textContent = "Create New System Prompt";
idInput.value = "";
nameInput.value = "";
contentInput.value = "";
}
modal.classList.add("visible");
}
function closePromptModal() {
document.getElementById("prompt-modal").classList.remove("visible");
}
function savePromptFromModal() {
const id = document.getElementById("modal-prompt-id").value;
const name = document.getElementById("modal-prompt-name").value.trim();
const content = document
.getElementById("modal-prompt-content")
.value.trim();
if (!name) {
showNotification("Prompt name cannot be empty.", "error");
return;
}
const promptId = id || `custom-${Date.now()}`;
const now = Date.now();
AppState.systemPrompts[promptId] = {
name,
content,
createdAt: AppState.systemPrompts[promptId]?.createdAt || now,
updatedAt: now,
};
renderSystemPromptsList();
// Re-render both chat views to update their prompt dropdowns
if (AppState.activeChatViews[0].conversationId) renderActiveConversation(1);
if (AppState.activeChatViews[1].conversationId && AppState.splitView) renderActiveConversation(2);
saveToStorage();
closePromptModal();
showNotification("Prompt saved!", "success");
}
// --- Notebook Management ---
function renderNotebookList() {
const container = document.getElementById("notebook-list");
container.innerHTML = "";
// Sort notes by creation date, newest first
const sortedNotes = [...AppState.notebook].sort(
(a, b) => b.createdAt - a.createdAt,
);
sortedNotes.forEach((note) => {
const div = document.createElement("div");
div.className = "list-item";
div.onclick = () => openNoteModal(note.id);
div.innerHTML = `
<div class="list-item-header">
<span class="list-item-name">Note</span>
<div class="list-item-actions">
<button class="btn-icon" title="Delete" onclick="event.stopPropagation(); deleteNote('${note.id}')">🗑️</button>
</div>
</div>
<div class="list-item-preview">${note.content}</div>
`;
container.appendChild(div);
});
}
function openNoteModal(noteId = null) {
const modal = document.getElementById("note-modal");
const title = document.getElementById("note-modal-title");
const idInput = document.getElementById("modal-note-id");
const contentInput = document.getElementById("modal-note-content");
if (noteId && AppState.notebook.find((n) => n.id === noteId)) {
const note = AppState.notebook.find((n) => n.id === noteId);
title.textContent = "Edit Note";
idInput.value = noteId;
contentInput.value = note.content;
} else {
title.textContent = "Create New Note";
idInput.value = "";
contentInput.value = "";
}
modal.classList.add("visible");
}
function closeNoteModal() {
document.getElementById("note-modal").classList.remove("visible");
}
function saveNoteFromModal() {
const id = document.getElementById("modal-note-id").value;
const content = document
.getElementById("modal-note-content")
.value.trim();
if (!content) return;
if (id) {
const note = AppState.notebook.find((n) => n.id === id);
if (note) note.content = content;
} else {
AppState.notebook.unshift({
id: `note-${Date.now()}`,
content: content,
createdAt: Date.now(), // Add creation timestamp
});
}
renderNotebookList();
saveToStorage();
closeNoteModal();
}
function deleteNote(noteId) {
if (confirm("Are you sure you want to delete this note?")) {
AppState.notebook = AppState.notebook.filter((n) => n.id !== noteId);
renderNotebookList();
saveToStorage();
}
}
// --- Utility Functions ---
function updateTokenCount(chatViewIndex = 1) {
const text = document.getElementById(`user-input-${chatViewIndex}`).value;
const tokenCount = Math.ceil(text.length / 4); // Rough approximation
document.getElementById(
`token-counter-${chatViewIndex}`,
).textContent = `Tokens: ${tokenCount}`;
}
function updateModelOptions() {
const modelSelect = document.getElementById("model");
modelSelect.innerHTML = "";
PROVIDERS.deepseek.models.forEach((model) => {
const option = document.createElement("option");
option.value = model;
option.textContent = model;
modelSelect.appendChild(option);
});
}
function showNotification(message, type = "info") {
const notification = document.createElement("div");
notification.style.cssText = `
position: fixed; top: 20px; right: 20px; padding: 12px 20px;
border-radius: 6px; color: white; font-weight: 500; z-index: 10001;
opacity: 0; transition: opacity 0.3s, transform 0.3s; transform: translateY(-20px);
background: ${
type === "success" ?
"var(--success)" :
type === "error" ?
"var(--danger)" :
"var(--accent)"
};
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = "1";
notification.style.transform = "translateY(0)";
}, 10);
setTimeout(() => {
notification.style.opacity = "0";
notification.style.transform = "translateY(-20px)";
setTimeout(() => document.body.removeChild(notification), 300);
}, 3000);
}
function downloadFile(content, filename, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function exportChatMD(chatViewIndex = 1) {
const viewState = AppState.activeChatViews[chatViewIndex - 1];
const conv = AppState.conversations[viewState.conversationId];
if (!conv) {
showNotification("No conversation selected for export.", "error");
return;
}
const systemPrompt = AppState.systemPrompts[viewState.systemPromptId];
let mdContent = `# Chat Export: ${conv.title}\n\n`;
mdContent += `**Model:** ${conv.parameters.model}\n`;
mdContent += `**Temperature:** ${conv.parameters.temperature}\n`;
mdContent += `**System Prompt: ${systemPrompt.name}**\n`;
mdContent += `\`\`\`\n${systemPrompt.content}\n\`\`\`\n\n---\n\n`;
conv.messages.forEach((msg) => {
const role = msg.role === "user" ? "You" : "Assistant";
mdContent += `### **${role}**\n\n`;
mdContent += `${msg.content}\n\n---\n\n`;
});
downloadFile(
mdContent,
`${conv.title.replace(/\s/g, "_")}.md`,
"text/markdown",
);
showNotification("Conversation exported to Markdown!", "success");
}
// --- New: Split View Functionality ---
function toggleSplitView() {
AppState.splitView = !AppState.splitView;
const chatView2 = document.getElementById("chat-view-2");
if (AppState.splitView) {
chatView2.style.display = "flex";
// If view 2 has no conversation, and view 1 has one, mirror view 1
if (!AppState.activeChatViews[1].conversationId && AppState.activeChatViews[0].conversationId) {
AppState.activeChatViews[1].conversationId = AppState.activeChatViews[0].conversationId;
// No need to change systemPromptId for view 2, it keeps its own or default.
}
// Always re-render view 2 to ensure correct display
renderActiveConversation(2);
} else {
chatView2.style.display = "none";
AppState.activeChatViews[1].conversationId = null; // Clear conversation for hidden view 2
AppState.activeChatViews[1].systemPromptId = "default"; // Reset prompt for hidden view 2
}
saveToStorage();
// renderAll(); // No need for full renderAll, specific renders are enough
showNotification(
`Split view ${AppState.splitView ? "enabled" : "disabled"}.`,
"info",
);
}
// --- Generic Generate Log Functionality (All "Generate X" buttons call this) ---
async function generateLog(chatViewIndex, buttonElement) {
const viewState = AppState.activeChatViews[chatViewIndex - 1];
const conversation = AppState.conversations[viewState.conversationId];
if (!conversation) {
showNotification("No active conversation selected.", "error");
return;
}
const promptId = buttonElement.dataset.promptId;
if (!promptId || !AppState.systemPrompts[promptId]) {
showNotification(
"Error: Invalid prompt ID for log generation. Check data-prompt-id on button.",
"error",
);
return;
}
const sendBtn = document.getElementById(`send-btn-${chatViewIndex}`);
const originalBtnHTML = buttonElement.innerHTML;
const originalSendBtnHTML = sendBtn.innerHTML;
// Show loading on the clicked button and disable it
buttonElement.innerHTML = '<div class="loading"></div>';
buttonElement.disabled = true;
// Also disable the main send button while generation is in progress
sendBtn.innerHTML = '<div class="loading"></div>';
sendBtn.disabled = true;
// Store current system prompt ID for this view to restore it later
const originalSystemPromptId = viewState.systemPromptId;
// Temporarily set the system prompt for THIS VIEW to the selected log generator's prompt
viewState.systemPromptId = promptId;
try {
const response = await callAPI(chatViewIndex); // Call API with the temporarily changed system prompt for this view
conversation.messages.push({
role: "assistant",
content: response,
promptId: promptId, // Store the promptId with the message for BRF export and potential future rendering hints
});
} catch (error) {
console.error("API call error during log generation:", error);
conversation.messages.push({
role: "assistant",
content: `❌ Error generating log: ${error.message}`,
isError: true,
promptId: 'error',
});
} finally {
// Restore the original system prompt for the conversation view
viewState.systemPromptId = originalSystemPromptId;
renderActiveConversation(chatViewIndex); // Re-render chat with new message
// Restore button states
buttonElement.innerHTML = originalBtnHTML;
buttonElement.disabled = false;
sendBtn.innerHTML = originalSendBtnHTML;
sendBtn.disabled = false;
saveToStorage(); // Save updated conversation to storage
}
}
// --- BRF Import/Export UI Functions ---
function exportBRF() {
try {
const brfData = brfManager.generateBRF({
studioVersion: "BAIRC Chat Studio 2.1.0",
userId: "user_" + Math.random().toString(36).substr(2, 5),
});
downloadFile(
brfData,
`BAIRC_Export_${new Date().toISOString().replace(/[:.-]/g, "")}.json`,
"application/json",
);
showNotification("BRF data exported successfully!", "success");
} catch (error) {
console.error("BRF Export Error:", error);
showNotification(`BRF Export failed: ${error.message}`, "error");
}
}
async function handleBRFImport(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
const brfContent = e.target.result;
await brfManager.importBRF(brfContent, { createBackup: true });
showNotification("BRF data imported successfully!", "success");
renderAll(); // Re-render the entire UI to reflect imported data
} catch (error) {
console.error("BRF Import Error:", error);
showNotification(`BRF Import failed: ${error.message}`, "error");
}
event.target.value = ""; // Clear file input
};
reader.readAsText(file);
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=ninjacricket/sorin-s-cat" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>