Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,22 @@ GITHUB_REPO=tbuczen/LARPilot
# GitHub Discussion Category ID for questions/general feedback
# Get via GraphQL: https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions
GITHUB_DISCUSSION_CATEGORY_ID=your-category-id-here
###< GitHub Integration
###< GitHub Integration

###> OpenAI Integration (StoryAI)
# OpenAI API Key for RAG-based story assistance
# Get your key at: https://platform.openai.com/api-keys
OPENAI_API_KEY=your-openai-api-key-here
# Models (optional, defaults shown)
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
OPENAI_COMPLETION_MODEL=gpt-4o-mini
# Enable auto-indexing of story objects on save (false by default to save costs)
STORY_AI_AUTO_INDEX=false

# Vector Store for AI embeddings (CQRS - external database)
# Format: supabase://SERVICE_KEY@PROJECT_REF
# Example: supabase://eyJhbGciOiJIUzI1...@abc123def
# Leave empty to disable vector store (AI features will be limited)
# See docs/VECTOR_STORE_SETUP.md for setup instructions
VECTOR_STORE_DSN=
###< OpenAI Integration (StoryAI)
61 changes: 54 additions & 7 deletions .serena/project.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: php

# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
Expand Down Expand Up @@ -63,5 +57,58 @@ excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""

# the name by which the project can be referenced within Serena
project_name: "LARPilot"

# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:

# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:

# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []

# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []

# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: utf-8


# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# powershell python python_jedi r rego
# ruby ruby_solargraph rust scala swift
# terraform toml typescript typescript_vts vue
# yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- php
2 changes: 2 additions & 0 deletions assets/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SortableCharacterChoicesController from "./controllers/sortable_character
import TimelineController from "./controllers/timeline_controller.js";
import DeleteConfirmController from "./controllers/delete-confirm_controller.js";
import GooglePlacesAutocompleteController from "./controllers/google-places-autocomplete_controller.js";
import AIAssistantController from "./controllers/ai_assistant_controller.js";

const app = startStimulusApp();
app.register('live', LiveController);
Expand All @@ -28,3 +29,4 @@ app.register("sortable-character-choices", SortableCharacterChoicesController);
app.register("timeline", TimelineController);
app.register("delete-confirm", DeleteConfirmController);
app.register("google-places-autocomplete", GooglePlacesAutocompleteController);
app.register("ai-assistant", AIAssistantController);
199 changes: 199 additions & 0 deletions assets/controllers/ai_assistant_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
static targets = [
'messagesContainer',
'messageInput',
'sendButton',
'typingIndicator',
'errorDisplay',
'errorMessage',
'sourcesPanel',
'sourcesList',
];

static values = {
queryUrl: String,
larpTitle: String,
};

connect() {
this.conversationHistory = [];
this.isProcessing = false;
this.scrollToBottom();
}

async sendMessage(event) {
event.preventDefault();

if (this.isProcessing || !this.hasMessageInputTarget) {
return;
}

const query = this.messageInputTarget.value.trim();
if (!query) {
return;
}

// Clear input and disable
this.messageInputTarget.value = '';
this.isProcessing = true;
this.sendButtonTarget.disabled = true;
this.hideError();

// Append user message
this.appendMessage('user', query);
this.conversationHistory.push({ role: 'user', content: query });

// Show typing indicator
this.typingIndicatorTarget.style.display = 'flex';
this.scrollToBottom();

try {
const response = await fetch(this.queryUrlValue, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query: query,
history: this.conversationHistory.slice(0, -1), // exclude current query
}),
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Request failed (${response.status})`);
}

const data = await response.json();

// Append assistant response
this.appendMessage('assistant', data.response);
this.conversationHistory.push({ role: 'assistant', content: data.response });

// Render sources
if (data.sources && data.sources.length > 0) {
this.renderSources(data.sources);
} else {
this.sourcesPanelTarget.style.display = 'none';
}
} catch (error) {
this.showError(error.message || 'Failed to get a response. Please try again.');
} finally {
this.typingIndicatorTarget.style.display = 'none';
this.isProcessing = false;
this.sendButtonTarget.disabled = false;
this.messageInputTarget.focus();
this.scrollToBottom();
}
}

appendMessage(role, content) {
const messageDiv = document.createElement('div');
messageDiv.className = `ai-chat-message ai-chat-message--${role}`;

const avatarDiv = document.createElement('div');
avatarDiv.className = 'ai-chat-avatar';

if (role === 'user') {
avatarDiv.innerHTML = '<i class="bi bi-person"></i>';
} else {
avatarDiv.innerHTML = '<i class="bi bi-robot"></i>';
}

const bubbleDiv = document.createElement('div');
bubbleDiv.className = 'ai-chat-bubble';

if (role === 'user') {
bubbleDiv.textContent = content;
} else {
bubbleDiv.innerHTML = this.renderMarkdown(content);
}

messageDiv.appendChild(avatarDiv);
messageDiv.appendChild(bubbleDiv);
this.messagesContainerTarget.appendChild(messageDiv);
this.scrollToBottom();
}

renderMarkdown(text) {
// Escape HTML first
let html = this.escapeHtml(text);

// Code blocks (``` ... ```)
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');

// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');

// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');

// Italic
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');

// Line breaks
html = html.replace(/\n/g, '<br>');

return html;
}

renderSources(sources) {
this.sourcesListTarget.innerHTML = '';
sources.forEach(source => {
const badge = document.createElement('span');
badge.className = 'badge bg-secondary me-1 mb-1';
badge.title = source.preview || '';
badge.textContent = `${source.type}: ${source.title} (${source.similarity}%)`;
this.sourcesListTarget.appendChild(badge);
});
this.sourcesPanelTarget.style.display = 'block';
}

clearHistory() {
this.conversationHistory = [];

// Remove all messages except the welcome message
const messages = this.messagesContainerTarget.querySelectorAll('.ai-chat-message');
messages.forEach(msg => {
if (!msg.hasAttribute('data-welcome-message')) {
msg.remove();
}
});

this.sourcesPanelTarget.style.display = 'none';
this.hideError();
this.scrollToBottom();
}

handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.sendMessage(event);
}
}

showError(message) {
this.errorMessageTarget.textContent = message;
this.errorDisplayTarget.style.display = 'block';
this.scrollToBottom();
}

hideError() {
this.errorDisplayTarget.style.display = 'none';
}

scrollToBottom() {
if (this.hasMessagesContainerTarget) {
this.messagesContainerTarget.scrollTop = this.messagesContainerTarget.scrollHeight;
}
}

escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
1 change: 1 addition & 0 deletions assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@import "./components/timeline";
@import "./components/map_preview";
@import "./components/speech_to_text";
@import "./components/ai_assistant";
@import "./vendors/quill.snow";
@import "./vendors/vis-timeline-graph2d.min";

Expand Down
Loading
Loading