Compare commits
No commits in common. "614176d2a9c06e622c48167aff3ad9060732ace5" and "93f8c7bb33577e93824fbf04241ab09e388cc4df" have entirely different histories.
614176d2a9
...
93f8c7bb33
|
|
@ -17,22 +17,6 @@ rojo-chat-panel {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rcp-picker {
|
|
||||||
flex: 1;
|
|
||||||
background: #1a1d2a;
|
|
||||||
color: #dde0f0;
|
|
||||||
border: 1px solid #2a2d3a;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-family: inherit;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rcp-picker:focus { border-color: #3a4a8a; }
|
|
||||||
|
|
||||||
.rcp-toolbar-icon {
|
.rcp-toolbar-icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Editor } from '../../editor/Editor.js';
|
|
||||||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
||||||
|
|
||||||
declare const markdownit: ( options?: Record<string, unknown> ) => { render: ( md: string ) => string };
|
declare const markdownit: ( options?: Record<string, unknown> ) => { render: ( md: string ) => string };
|
||||||
|
|
@ -13,22 +12,12 @@ function extractRawText( node: Node ): string
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RojoEntry { id: string; name: string; description: string; path: string; }
|
|
||||||
|
|
||||||
function parentDir( filePath: string ): string
|
|
||||||
{
|
|
||||||
const slash = filePath.lastIndexOf( '/' );
|
|
||||||
return slash >= 0 ? filePath.slice( 0, slash ) : 'workspace/rojos';
|
|
||||||
}
|
|
||||||
|
|
||||||
class RojoChatPanel extends HTMLElement
|
class RojoChatPanel extends HTMLElement
|
||||||
{
|
{
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
_conversationId = '';
|
_conversationId: string = '';
|
||||||
_sending = false;
|
_sending = false;
|
||||||
_md: { render: ( s: string ) => string } | null = null;
|
_md: { render: ( s: string ) => string } | null = null;
|
||||||
_rojos: RojoEntry[] = [];
|
|
||||||
_selectedPath = '';
|
|
||||||
|
|
||||||
connectedCallback(): void
|
connectedCallback(): void
|
||||||
{
|
{
|
||||||
|
|
@ -42,11 +31,9 @@ class RojoChatPanel extends HTMLElement
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<div class="rcp-toolbar">
|
<div class="rcp-toolbar">
|
||||||
<div class="rcp-toolbar-icon">🤖</div>
|
<div class="rcp-toolbar-icon">🤖</div>
|
||||||
<select class="rcp-picker" title="Select Rojo">
|
<span class="rcp-toolbar-name">Rojo</span>
|
||||||
<option value="">— no rojo selected —</option>
|
<button class="rcp-toolbar-btn" title="Actions">★</button>
|
||||||
</select>
|
<button class="rcp-toolbar-btn" title="Settings">☰</button>
|
||||||
<button class="rcp-toolbar-btn rcp-new-btn" title="New Rojo">+</button>
|
|
||||||
<button class="rcp-toolbar-btn rcp-refresh-btn" title="Refresh rojo list">↻</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="rcp-history"></div>
|
<div class="rcp-history"></div>
|
||||||
<div class="rcp-input-area">
|
<div class="rcp-input-area">
|
||||||
|
|
@ -60,101 +47,26 @@ class RojoChatPanel extends HTMLElement
|
||||||
|
|
||||||
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
||||||
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
||||||
const picker = this.querySelector( '.rcp-picker' ) as HTMLSelectElement;
|
|
||||||
|
|
||||||
sendBtn.addEventListener( 'click', () => this._send() );
|
sendBtn.addEventListener( 'click', () => this._send() );
|
||||||
|
|
||||||
inputText.addEventListener( 'keydown', ( e: KeyboardEvent ) =>
|
inputText.addEventListener( 'keydown', ( e: KeyboardEvent ) =>
|
||||||
{
|
{
|
||||||
if ( e.key === 'Enter' && !e.shiftKey ) { e.preventDefault(); this._send(); }
|
if ( e.key === 'Enter' && !e.shiftKey )
|
||||||
} );
|
|
||||||
|
|
||||||
picker.addEventListener( 'change', () =>
|
|
||||||
{
|
|
||||||
this._selectedPath = picker.value;
|
|
||||||
} );
|
|
||||||
|
|
||||||
this.querySelector( '.rcp-new-btn' )!.addEventListener( 'click', () => this._createRojo() );
|
|
||||||
this.querySelector( '.rcp-refresh-btn' )!.addEventListener( 'click', () => this._loadRojos() );
|
|
||||||
|
|
||||||
this._loadRojos();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _loadRojos(): Promise<void>
|
|
||||||
{
|
|
||||||
const projectId = Editor.get().projectId;
|
|
||||||
if ( !projectId ) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const res = await fetch( `/api/rojos/${ projectId }/list` );
|
|
||||||
if ( !res.ok ) return;
|
|
||||||
this._rojos = await res.json() as RojoEntry[];
|
|
||||||
this._renderPicker();
|
|
||||||
}
|
|
||||||
catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderPicker(): void
|
|
||||||
{
|
|
||||||
const picker = this.querySelector( '.rcp-picker' ) as HTMLSelectElement;
|
|
||||||
const prev = this._selectedPath;
|
|
||||||
|
|
||||||
picker.innerHTML = '<option value="">— no rojo selected —</option>' +
|
|
||||||
this._rojos.map( r =>
|
|
||||||
`<option value="${ r.path }">${ r.name || r.path.slice( r.path.lastIndexOf( '/' ) + 1 ) }</option>`
|
|
||||||
).join( '' );
|
|
||||||
|
|
||||||
if ( prev && this._rojos.some( r => r.path === prev ) )
|
|
||||||
{
|
|
||||||
picker.value = prev;
|
|
||||||
this._selectedPath = prev;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this._selectedPath = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _createRojo(): Promise<void>
|
|
||||||
{
|
|
||||||
const projectId = Editor.get().projectId;
|
|
||||||
if ( !projectId ) return;
|
|
||||||
|
|
||||||
// Place new rojo in the same directory as the currently selected one,
|
|
||||||
// or directly in workspace/rojos if nothing is selected.
|
|
||||||
const dir = this._selectedPath
|
|
||||||
? parentDir( this._selectedPath )
|
|
||||||
: 'workspace/rojos';
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const res = await fetch( `/api/rojos/${ projectId }/create`,
|
|
||||||
{
|
{
|
||||||
method: 'POST',
|
e.preventDefault();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
this._send();
|
||||||
body: JSON.stringify( { parentDir: dir } ),
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
if ( !res.ok ) return;
|
|
||||||
const { path: newPath } = await res.json() as { id: string; path: string; name: string };
|
|
||||||
|
|
||||||
await this._loadRojos();
|
|
||||||
this._selectedPath = newPath;
|
|
||||||
( this.querySelector( '.rcp-picker' ) as HTMLSelectElement ).value = newPath;
|
|
||||||
|
|
||||||
await Editor.get().openDocument( newPath );
|
|
||||||
}
|
|
||||||
catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _send(): Promise<void>
|
async _send(): Promise<void>
|
||||||
{
|
{
|
||||||
if ( this._sending ) return;
|
if ( this._sending ) return;
|
||||||
|
|
||||||
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
||||||
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
||||||
const history = this.querySelector( '.rcp-history' ) as HTMLElement;
|
const history = this.querySelector( '.rcp-history' ) as HTMLElement;
|
||||||
|
|
||||||
const text = extractRawText( inputText ).replaceAll( ' ', ' ' ).trim();
|
const text = extractRawText( inputText ).replaceAll( ' ', ' ' ).trim();
|
||||||
if ( !text ) return;
|
if ( !text ) return;
|
||||||
|
|
@ -163,8 +75,8 @@ class RojoChatPanel extends HTMLElement
|
||||||
this._sending = true;
|
this._sending = true;
|
||||||
sendBtn.disabled = true;
|
sendBtn.disabled = true;
|
||||||
|
|
||||||
const userBubble = document.createElement( 'div' );
|
const userBubble = document.createElement( 'div' );
|
||||||
userBubble.className = 'rcp-user-bubble';
|
userBubble.className = 'rcp-user-bubble';
|
||||||
userBubble.textContent = text;
|
userBubble.textContent = text;
|
||||||
history.appendChild( userBubble );
|
history.appendChild( userBubble );
|
||||||
history.scrollTop = history.scrollHeight;
|
history.scrollTop = history.scrollHeight;
|
||||||
|
|
@ -177,20 +89,13 @@ class RojoChatPanel extends HTMLElement
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const body: Record<string, string> = { id: this._conversationId, message: text };
|
|
||||||
|
|
||||||
if ( this._selectedPath )
|
|
||||||
{
|
|
||||||
body.projectId = Editor.get().projectId;
|
|
||||||
body.rojoPath = this._selectedPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch( '/api/rojos/chat',
|
const response = await fetch( '/api/rojos/chat',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify( body ),
|
body: JSON.stringify( { id: this._conversationId, message: text } ),
|
||||||
} );
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if ( !response.body ) throw new Error( 'No response body' );
|
if ( !response.body ) throw new Error( 'No response body' );
|
||||||
|
|
||||||
|
|
@ -205,6 +110,8 @@ class RojoChatPanel extends HTMLElement
|
||||||
|
|
||||||
const raw = decoder.decode( value, { stream: true } );
|
const raw = decoder.decode( value, { stream: true } );
|
||||||
|
|
||||||
|
console.log( "Received raw:", raw );
|
||||||
|
|
||||||
for ( const line of raw.split( '\n' ) )
|
for ( const line of raw.split( '\n' ) )
|
||||||
{
|
{
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
@ -213,6 +120,7 @@ class RojoChatPanel extends HTMLElement
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const msg = JSON.parse( trimmed ) as { type: string; text?: string };
|
const msg = JSON.parse( trimmed ) as { type: string; text?: string };
|
||||||
|
|
||||||
if ( msg.type === 'CHAT' && msg.text )
|
if ( msg.type === 'CHAT' && msg.text )
|
||||||
{
|
{
|
||||||
markdown += msg.text;
|
markdown += msg.text;
|
||||||
|
|
@ -220,11 +128,32 @@ class RojoChatPanel extends HTMLElement
|
||||||
history.scrollTop = history.scrollHeight;
|
history.scrollTop = history.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{
|
||||||
|
// partial or non-JSON chunk — skip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !markdown ) assistantBubble.textContent = '(no response)';
|
const remaining = decoder.decode();
|
||||||
|
if ( remaining.trim() )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const msg = JSON.parse( remaining.trim() ) as { type: string; text?: string };
|
||||||
|
if ( msg.type === 'CHAT' && msg.text )
|
||||||
|
{
|
||||||
|
markdown += msg.text;
|
||||||
|
assistantBubble.innerHTML = this._md!.render( markdown );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !markdown )
|
||||||
|
{
|
||||||
|
assistantBubble.textContent = '(no response)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch ( err )
|
catch ( err )
|
||||||
{
|
{
|
||||||
|
|
@ -239,8 +168,7 @@ class RojoChatPanel extends HTMLElement
|
||||||
|
|
||||||
addContextMenuEntries( dir: ContextMenuDirectory ): void
|
addContextMenuEntries( dir: ContextMenuDirectory ): void
|
||||||
{
|
{
|
||||||
const rojo = this._rojos.find( r => r.path === this._selectedPath );
|
dir.add( new ContextMenuReadOnlyEntry( dir, `Rojo Chat — ${this._conversationId.slice( 0, 8 )}` ) );
|
||||||
dir.add( new ContextMenuReadOnlyEntry( dir, `Rojo: ${ rojo?.name ?? 'none' }` ) );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,294 +0,0 @@
|
||||||
rojo-settings-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
background: #0f1117;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Toolbar ─────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.rsp-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #13151f;
|
|
||||||
border-bottom: 1px solid #2a2d3a;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-toolbar-title {
|
|
||||||
flex: 1;
|
|
||||||
color: #e2e4ed;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-save {
|
|
||||||
padding: 4px 14px;
|
|
||||||
background: #1a5fd4;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-save:hover:not(:disabled) { background: #1450b0; }
|
|
||||||
|
|
||||||
.rsp-save:disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Empty state ─────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.rsp-empty {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #555;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Portrait ────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.rsp-portrait-wrap {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 8px 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-portrait {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #0a0c12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-portrait svg {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Form ────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.rsp-form {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-section-title {
|
|
||||||
color: #7c8cff;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
border-bottom: 1px solid #1e2235;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-name-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-name-label {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-sync-btn {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #1e2235;
|
|
||||||
border: 1px solid #2a2d3a;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #8890a8;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-sync-btn:hover { background: #252a40; color: #c0c6de; }
|
|
||||||
|
|
||||||
.rsp-id-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 2px 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-id-label {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.07em;
|
|
||||||
color: #555;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-id-value {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-family: ui-monospace, "Cascadia Code", monospace;
|
|
||||||
color: #555;
|
|
||||||
word-break: break-all;
|
|
||||||
user-select: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
color: #8890a8;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-input {
|
|
||||||
background: #13151f;
|
|
||||||
color: #dde0f0;
|
|
||||||
border: 1px solid #2a2d3a;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 7px 10px;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-family: inherit;
|
|
||||||
outline: none;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-input:focus {
|
|
||||||
border-color: #3a4a8a;
|
|
||||||
background: #141620;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-textarea {
|
|
||||||
resize: vertical;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-system-prompt {
|
|
||||||
min-height: 120px;
|
|
||||||
font-family: ui-monospace, "Cascadia Code", monospace;
|
|
||||||
font-size: 0.83rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
select.rsp-input {
|
|
||||||
appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-endpoint-external,
|
|
||||||
.rsp-endpoint-tunnel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Tunnel picker ───────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.rsp-tunnel-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-id-label { flex: 1; min-width: 0; }
|
|
||||||
|
|
||||||
.rsp-browse-tunnels-btn {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #1e2235;
|
|
||||||
border: 1px solid #2a2d3a;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #8890a8;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-browse-tunnels-btn:hover { background: #252a40; color: #c0c6de; }
|
|
||||||
|
|
||||||
.rsp-tunnel-picker {
|
|
||||||
border: 1px solid #1e2235;
|
|
||||||
border-radius: 7px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #0d0f18;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: 180px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-entry {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid #1a1d2a;
|
|
||||||
transition: background 0.12s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-entry:last-child { border-bottom: none; }
|
|
||||||
.rsp-tunnel-entry:hover { background: #141826; }
|
|
||||||
|
|
||||||
.rsp-tunnel-dot {
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #2a2d3a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-active .rsp-tunnel-dot {
|
|
||||||
background: #22c55e;
|
|
||||||
box-shadow: 0 0 5px #22c55e88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-name {
|
|
||||||
flex: 1;
|
|
||||||
color: #c0c6de;
|
|
||||||
font-size: 0.83rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-purpose {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: #555;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rsp-tunnel-loading,
|
|
||||||
.rsp-tunnel-empty {
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #555;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
@ -1,424 +0,0 @@
|
||||||
import { Editor } from '../../editor/Editor.js';
|
|
||||||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
|
||||||
|
|
||||||
const NS_INKSCAPE = 'http://www.inkscape.org/namespaces/inkscape';
|
|
||||||
|
|
||||||
function inkscapeLabel( el: Element ): string
|
|
||||||
{
|
|
||||||
return el.getAttributeNS( NS_INKSCAPE, 'label' ) ?? el.getAttribute( 'inkscape:label' ) ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
interface RojoEndpoint
|
|
||||||
{
|
|
||||||
type: 'external' | 'tunnel';
|
|
||||||
url: string;
|
|
||||||
model: string;
|
|
||||||
apiKey: string;
|
|
||||||
tunnelId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RojoColor
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
target: 'fill' | 'stroke' | 'both';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RojoLayer
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
selected: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RojoAppearance
|
|
||||||
{
|
|
||||||
colors: RojoColor[];
|
|
||||||
layers: RojoLayer[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RojoSettings
|
|
||||||
{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
systemPrompt: string;
|
|
||||||
endpoint: RojoEndpoint;
|
|
||||||
appearance: RojoAppearance;
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultSettings(): RojoSettings
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
systemPrompt: '',
|
|
||||||
endpoint: { type: 'external', url: '', model: '', apiKey: '', tunnelId: '' },
|
|
||||||
appearance: { colors: [], layers: [] },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Name normalization ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function normalizeName( name: string ): string
|
|
||||||
{
|
|
||||||
return name
|
|
||||||
.toLowerCase()
|
|
||||||
.normalize( 'NFD' )
|
|
||||||
.replace( /[̀-ͯ]/g, '' )
|
|
||||||
.replace( /[^a-z0-9\s-]/g, '' )
|
|
||||||
.trim()
|
|
||||||
.replace( /\s+/g, '-' )
|
|
||||||
.replace( /-+/g, '-' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── SVG processing ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const CHAT_MASK_VIEWBOX = '1.5883274 0.60429424 25.973324 23.865683';
|
|
||||||
|
|
||||||
async function loadPortraitSvg(): Promise<string>
|
|
||||||
{
|
|
||||||
const res = await fetch( '/rojos/rojo-base.svg' );
|
|
||||||
const text = await res.text();
|
|
||||||
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString( text, 'image/svg+xml' );
|
|
||||||
const svg = doc.documentElement;
|
|
||||||
|
|
||||||
doc.querySelector( 'sodipodi\\:namedview, [id="namedview7"]' )?.remove();
|
|
||||||
|
|
||||||
const toRemove: Element[] = [];
|
|
||||||
svg.querySelectorAll( '*' ).forEach( el =>
|
|
||||||
{
|
|
||||||
const label = inkscapeLabel( el );
|
|
||||||
if ( label.endsWith( '-pivot' ) || label === 'chat-cutout' || label === 'chat-mask' )
|
|
||||||
{
|
|
||||||
toRemove.push( el );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
toRemove.forEach( el => el.remove() );
|
|
||||||
|
|
||||||
svg.setAttribute( 'viewBox', CHAT_MASK_VIEWBOX );
|
|
||||||
svg.setAttribute( 'width', '100%' );
|
|
||||||
svg.setAttribute( 'height', '100%' );
|
|
||||||
|
|
||||||
return new XMLSerializer().serializeToString( svg );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Component ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class RojoSettingsPanel extends HTMLElement
|
|
||||||
{
|
|
||||||
currentPath: string | null = null;
|
|
||||||
_initialized = false;
|
|
||||||
_ignoreChange = false;
|
|
||||||
_appearance: RojoAppearance = { colors: [], layers: [] };
|
|
||||||
_rojoId = '';
|
|
||||||
|
|
||||||
connectedCallback(): void
|
|
||||||
{
|
|
||||||
if ( this._initialized ) return;
|
|
||||||
this._initialized = true;
|
|
||||||
|
|
||||||
this.className = 'rojo-settings-panel';
|
|
||||||
this.innerHTML = `
|
|
||||||
<div class="rsp-toolbar">
|
|
||||||
<span class="rsp-toolbar-title">🤖 Rojo Settings</span>
|
|
||||||
<button class="rsp-save" disabled>Save</button>
|
|
||||||
</div>
|
|
||||||
<div class="rsp-empty">Open a .rojo file from the file tree</div>
|
|
||||||
<div class="rsp-form" style="display:none">
|
|
||||||
<div class="rsp-portrait-wrap">
|
|
||||||
<div class="rsp-portrait"></div>
|
|
||||||
</div>
|
|
||||||
<div class="rsp-section">
|
|
||||||
<div class="rsp-name-row">
|
|
||||||
<label class="rsp-label rsp-name-label">Name
|
|
||||||
<input class="rsp-input" data-field="name" type="text" placeholder="My Rojo">
|
|
||||||
</label>
|
|
||||||
<button class="rsp-sync-btn" title="Rename file to match this name">Sync filename</button>
|
|
||||||
</div>
|
|
||||||
<div class="rsp-id-row">
|
|
||||||
<span class="rsp-id-label">ID</span>
|
|
||||||
<span class="rsp-id-value"></span>
|
|
||||||
</div>
|
|
||||||
<label class="rsp-label">Description
|
|
||||||
<textarea class="rsp-input rsp-textarea" data-field="description" placeholder="What this rojo does…" rows="2"></textarea>
|
|
||||||
</label>
|
|
||||||
<label class="rsp-label">System Prompt
|
|
||||||
<textarea class="rsp-input rsp-textarea rsp-system-prompt" data-field="systemPrompt" placeholder="You are a helpful assistant…" rows="6"></textarea>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="rsp-section">
|
|
||||||
<div class="rsp-section-title">Endpoint</div>
|
|
||||||
<label class="rsp-label">Type
|
|
||||||
<select class="rsp-input" data-field="endpoint.type">
|
|
||||||
<option value="external">External / Local URL</option>
|
|
||||||
<option value="tunnel">Tunnel (tunnel.rokojori.com)</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<div class="rsp-endpoint-external">
|
|
||||||
<label class="rsp-label">Base URL
|
|
||||||
<input class="rsp-input" data-field="endpoint.url" type="text" placeholder="http://localhost:11434/v1">
|
|
||||||
</label>
|
|
||||||
<label class="rsp-label">API Key
|
|
||||||
<input class="rsp-input" data-field="endpoint.apiKey" type="password" placeholder="sk-… (or leave blank for local)">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="rsp-endpoint-tunnel" style="display:none">
|
|
||||||
<div class="rsp-tunnel-row">
|
|
||||||
<label class="rsp-label rsp-tunnel-id-label">Tunnel ID
|
|
||||||
<input class="rsp-input" data-field="endpoint.tunnelId" type="text" placeholder="uuid of the tunnel">
|
|
||||||
</label>
|
|
||||||
<button class="rsp-browse-tunnels-btn" title="Browse available tunnels">Browse</button>
|
|
||||||
</div>
|
|
||||||
<div class="rsp-tunnel-picker" style="display:none">
|
|
||||||
<div class="rsp-tunnel-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label class="rsp-label">Model
|
|
||||||
<input class="rsp-input" data-field="endpoint.model" type="text" placeholder="gpt-4o / llama3 / gemma3 / …">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.querySelector( '.rsp-save' )!.addEventListener( 'click', () => this._save() );
|
|
||||||
this.querySelector( '.rsp-sync-btn' )!.addEventListener( 'click', () => this._syncFilename() );
|
|
||||||
this.querySelector( '.rsp-browse-tunnels-btn' )!.addEventListener( 'click', () => this._browseTunnels() );
|
|
||||||
|
|
||||||
this.querySelectorAll( '[data-field]' ).forEach( el =>
|
|
||||||
{
|
|
||||||
el.addEventListener( 'input', () => this._onFieldChange() );
|
|
||||||
} );
|
|
||||||
|
|
||||||
( this.querySelector( '[data-field="endpoint.type"]' ) as HTMLSelectElement )
|
|
||||||
.addEventListener( 'change', () => this._updateEndpointVisibility() );
|
|
||||||
|
|
||||||
Editor.get().onDocumentOpened.addListener( e =>
|
|
||||||
{
|
|
||||||
if ( 'rojo-settings-panel' !== e.editorTag ) return;
|
|
||||||
this._loadDocument( e.path, e.content );
|
|
||||||
} );
|
|
||||||
|
|
||||||
document.addEventListener( 'keydown', ( e: KeyboardEvent ) =>
|
|
||||||
{
|
|
||||||
if ( !this.currentPath ) return;
|
|
||||||
if ( e.ctrlKey && e.key === 's' ) { e.preventDefault(); this._save(); }
|
|
||||||
} );
|
|
||||||
|
|
||||||
this._initPortrait();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _browseTunnels(): Promise<void>
|
|
||||||
{
|
|
||||||
const picker = this.querySelector( '.rsp-tunnel-picker' ) as HTMLElement;
|
|
||||||
const list = this.querySelector( '.rsp-tunnel-list' ) as HTMLElement;
|
|
||||||
|
|
||||||
if ( picker.style.display !== 'none' ) { picker.style.display = 'none'; return; }
|
|
||||||
|
|
||||||
list.innerHTML = '<div class="rsp-tunnel-loading">Loading…</div>';
|
|
||||||
picker.style.display = '';
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const res = await fetch( '/api/rojos/tunnels/browse' );
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if ( !res.ok || !Array.isArray( data ) )
|
|
||||||
{
|
|
||||||
list.innerHTML = `<div class="rsp-tunnel-empty">Error: ${ data?.error ?? 'Unexpected response' }</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tunnels = data as Array<{ id: string; name: string; purpose: string; active: boolean; }>;
|
|
||||||
|
|
||||||
if ( !tunnels.length )
|
|
||||||
{
|
|
||||||
list.innerHTML = '<div class="rsp-tunnel-empty">No tunnels available. Add one in the Tunnel Agent app.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.innerHTML = tunnels.map( t => `
|
|
||||||
<div class="rsp-tunnel-entry ${ t.active ? 'rsp-tunnel-active' : '' }" data-tunnel-id="${ t.id }">
|
|
||||||
<span class="rsp-tunnel-dot"></span>
|
|
||||||
<span class="rsp-tunnel-name">${ t.name }</span>
|
|
||||||
<span class="rsp-tunnel-purpose">${ t.purpose }</span>
|
|
||||||
</div>
|
|
||||||
` ).join( '' );
|
|
||||||
|
|
||||||
list.querySelectorAll( '.rsp-tunnel-entry' ).forEach( entry =>
|
|
||||||
{
|
|
||||||
entry.addEventListener( 'click', () =>
|
|
||||||
{
|
|
||||||
const id = ( entry as HTMLElement ).dataset.tunnelId ?? '';
|
|
||||||
this._setField( 'endpoint.tunnelId', id );
|
|
||||||
this._onFieldChange();
|
|
||||||
picker.style.display = 'none';
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
list.innerHTML = '<div class="rsp-tunnel-empty">Could not reach tunnel server.</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _initPortrait(): Promise<void>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const svgHtml = await loadPortraitSvg();
|
|
||||||
( this.querySelector( '.rsp-portrait' ) as HTMLElement ).innerHTML = svgHtml;
|
|
||||||
}
|
|
||||||
catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadDocument( path: string, content: string ): void
|
|
||||||
{
|
|
||||||
this.currentPath = path;
|
|
||||||
this._updateTabLabel( path );
|
|
||||||
|
|
||||||
let settings: RojoSettings = defaultSettings();
|
|
||||||
try { settings = JSON.parse( content ); }
|
|
||||||
catch {}
|
|
||||||
|
|
||||||
this._rojoId = settings.id ?? '';
|
|
||||||
this._appearance = settings.appearance ?? { colors: [], layers: [] };
|
|
||||||
|
|
||||||
this._ignoreChange = true;
|
|
||||||
this._populateForm( settings );
|
|
||||||
this._ignoreChange = false;
|
|
||||||
|
|
||||||
( this.querySelector( '.rsp-empty' ) as HTMLElement ).style.display = 'none';
|
|
||||||
( this.querySelector( '.rsp-form' ) as HTMLElement ).style.display = '';
|
|
||||||
this._updateSaveButton( false );
|
|
||||||
}
|
|
||||||
|
|
||||||
_populateForm( s: RojoSettings ): void
|
|
||||||
{
|
|
||||||
this._setField( 'name', s.name ?? '' );
|
|
||||||
this._setField( 'description', s.description ?? '' );
|
|
||||||
this._setField( 'systemPrompt', s.systemPrompt ?? '' );
|
|
||||||
this._setField( 'endpoint.type', s.endpoint?.type ?? 'external' );
|
|
||||||
this._setField( 'endpoint.url', s.endpoint?.url ?? '' );
|
|
||||||
this._setField( 'endpoint.apiKey', s.endpoint?.apiKey ?? '' );
|
|
||||||
this._setField( 'endpoint.tunnelId', s.endpoint?.tunnelId ?? '' );
|
|
||||||
this._setField( 'endpoint.model', s.endpoint?.model ?? '' );
|
|
||||||
|
|
||||||
( this.querySelector( '.rsp-id-value' ) as HTMLElement ).textContent =
|
|
||||||
s.id ? s.id : '—';
|
|
||||||
|
|
||||||
this._updateEndpointVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
_setField( field: string, value: string ): void
|
|
||||||
{
|
|
||||||
const el = this.querySelector( `[data-field="${ field }"]` ) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
||||||
if ( el ) el.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getField( field: string ): string
|
|
||||||
{
|
|
||||||
const el = this.querySelector( `[data-field="${ field }"]` ) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
||||||
return el ? el.value : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
_readSettings(): RojoSettings
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
id: this._rojoId,
|
|
||||||
name: this._getField( 'name' ),
|
|
||||||
description: this._getField( 'description' ),
|
|
||||||
systemPrompt: this._getField( 'systemPrompt' ),
|
|
||||||
endpoint: {
|
|
||||||
type: this._getField( 'endpoint.type' ) as 'external' | 'tunnel',
|
|
||||||
url: this._getField( 'endpoint.url' ),
|
|
||||||
apiKey: this._getField( 'endpoint.apiKey' ),
|
|
||||||
tunnelId: this._getField( 'endpoint.tunnelId' ),
|
|
||||||
model: this._getField( 'endpoint.model' ),
|
|
||||||
},
|
|
||||||
appearance: this._appearance,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_onFieldChange(): void
|
|
||||||
{
|
|
||||||
if ( this._ignoreChange || !this.currentPath ) return;
|
|
||||||
Editor.get().markDirty( this.currentPath, JSON.stringify( this._readSettings(), null, 2 ) );
|
|
||||||
this._updateSaveButton( true );
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateEndpointVisibility(): void
|
|
||||||
{
|
|
||||||
const type = this._getField( 'endpoint.type' );
|
|
||||||
( this.querySelector( '.rsp-endpoint-external' ) as HTMLElement ).style.display = type === 'external' ? '' : 'none';
|
|
||||||
( this.querySelector( '.rsp-endpoint-tunnel' ) as HTMLElement ).style.display = type === 'tunnel' ? '' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
async _syncFilename(): Promise<void>
|
|
||||||
{
|
|
||||||
if ( !this.currentPath ) return;
|
|
||||||
|
|
||||||
const name = this._getField( 'name' );
|
|
||||||
const slug = normalizeName( name );
|
|
||||||
if ( !slug ) return;
|
|
||||||
|
|
||||||
const newName = slug + '.rojo';
|
|
||||||
const oldPath = this.currentPath;
|
|
||||||
|
|
||||||
const res = await fetch( `/api/files/${ Editor.get().projectId }/rename`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify( { path: oldPath, newName } ),
|
|
||||||
} );
|
|
||||||
|
|
||||||
if ( !res.ok ) return;
|
|
||||||
|
|
||||||
// Derive new path: same directory, new filename
|
|
||||||
const dir = oldPath.lastIndexOf( '/' ) >= 0
|
|
||||||
? oldPath.slice( 0, oldPath.lastIndexOf( '/' ) )
|
|
||||||
: '';
|
|
||||||
const newPath = dir ? `${ dir }/${ newName }` : newName;
|
|
||||||
|
|
||||||
// Migrate openDocs entry to the new path
|
|
||||||
const editor = Editor.get();
|
|
||||||
const doc = editor.openDocs.get( oldPath );
|
|
||||||
if ( doc )
|
|
||||||
{
|
|
||||||
editor.openDocs.set( newPath, doc );
|
|
||||||
editor.openDocs.delete( oldPath );
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPath = newPath;
|
|
||||||
this._updateTabLabel( newPath );
|
|
||||||
}
|
|
||||||
|
|
||||||
async _save(): Promise<void>
|
|
||||||
{
|
|
||||||
if ( !this.currentPath ) return;
|
|
||||||
await Editor.get().save( this.currentPath );
|
|
||||||
this._updateSaveButton( false );
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateTabLabel( path: string ): void
|
|
||||||
{
|
|
||||||
const name = path ? path.slice( path.lastIndexOf( '/' ) + 1 ) : '';
|
|
||||||
this.dispatchEvent( new CustomEvent( 'panel:label-change', { bubbles: true, detail: { label: '🤖 ' + name } } ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateSaveButton( dirty: boolean ): void
|
|
||||||
{
|
|
||||||
( this.querySelector( '.rsp-save' ) as HTMLButtonElement ).disabled = !dirty;
|
|
||||||
}
|
|
||||||
|
|
||||||
addContextMenuEntries( dir: ContextMenuDirectory ): void
|
|
||||||
{
|
|
||||||
dir.add( new ContextMenuReadOnlyEntry( dir, this.currentPath ? `Rojo: ${ this.currentPath }` : 'No rojo open' ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define( 'rojo-settings-panel', RojoSettingsPanel );
|
|
||||||
|
|
@ -74,11 +74,10 @@ class TabContainer extends HTMLElement {
|
||||||
|
|
||||||
const addDir = new ContextMenuDirectory(root, 'Add');
|
const addDir = new ContextMenuDirectory(root, 'Add');
|
||||||
const panelTypes = [
|
const panelTypes = [
|
||||||
{ label: 'HTML Editor', panelType: 'html-editor', tag: 'html-editor-panel' },
|
{ label: 'HTML Editor', panelType: 'html-editor', tag: 'html-editor-panel' },
|
||||||
{ label: 'Code Editor', panelType: 'code-panel', tag: 'code-panel' },
|
{ label: 'Code Editor', panelType: 'code-panel', tag: 'code-panel' },
|
||||||
{ label: 'File Tree', panelType: 'file-tree', tag: 'file-tree-panel' },
|
{ label: 'File Tree', panelType: 'file-tree', tag: 'file-tree-panel' },
|
||||||
{ label: 'Rojo Chat', panelType: 'rojo-chat', tag: 'rojo-chat-panel' },
|
{ label: 'Rojo Chat', panelType: 'rojo-chat', tag: 'rojo-chat-panel' },
|
||||||
{ label: 'Rojo Settings', panelType: 'rojo-settings', tag: 'rojo-settings-panel' },
|
|
||||||
];
|
];
|
||||||
for (const pt of panelTypes) {
|
for (const pt of panelTypes) {
|
||||||
addDir.add(new ContextMenuEntry(addDir, pt.label, () => {
|
addDir.add(new ContextMenuEntry(addDir, pt.label, () => {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ export class FileEditorRegistry
|
||||||
{
|
{
|
||||||
static readonly DefaultEntries: RegistryEntry[] =
|
static readonly DefaultEntries: RegistryEntry[] =
|
||||||
[
|
[
|
||||||
{ suffix: 'rojo', editor: 'RojoSettingsPanel' },
|
|
||||||
{ suffix: 'html', editor: 'HTMLEditorPanel' },
|
{ suffix: 'html', editor: 'HTMLEditorPanel' },
|
||||||
{ suffix: 'htm', editor: 'HTMLEditorPanel' },
|
{ suffix: 'htm', editor: 'HTMLEditorPanel' },
|
||||||
{ suffix: 'js', editor: 'CodePanel' },
|
{ suffix: 'js', editor: 'CodePanel' },
|
||||||
|
|
@ -27,7 +26,6 @@ export class FileEditorRegistry
|
||||||
|
|
||||||
static readonly EditorTagNames: Record<string, string> =
|
static readonly EditorTagNames: Record<string, string> =
|
||||||
{
|
{
|
||||||
'RojoSettingsPanel': 'rojo-settings-panel',
|
|
||||||
'HTMLEditorPanel': 'html-editor-panel',
|
'HTMLEditorPanel': 'html-editor-panel',
|
||||||
'CodePanel': 'code-panel',
|
'CodePanel': 'code-panel',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
<link rel="stylesheet" href="/components/html-editor-panel/html-editor-panel.css">
|
<link rel="stylesheet" href="/components/html-editor-panel/html-editor-panel.css">
|
||||||
<link rel="stylesheet" href="/components/confirm-dialog/confirm-dialog.css">
|
<link rel="stylesheet" href="/components/confirm-dialog/confirm-dialog.css">
|
||||||
<link rel="stylesheet" href="/components/rojo-chat-panel/rojo-chat-panel.css">
|
<link rel="stylesheet" href="/components/rojo-chat-panel/rojo-chat-panel.css">
|
||||||
<link rel="stylesheet" href="/components/rojo-settings-panel/rojo-settings-panel.css">
|
|
||||||
<link rel="stylesheet" href="/vendor/codemirror.min.css">
|
<link rel="stylesheet" href="/vendor/codemirror.min.css">
|
||||||
<link rel="stylesheet" href="/components/code-panel/code-panel.css">
|
<link rel="stylesheet" href="/components/code-panel/code-panel.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -29,7 +28,6 @@
|
||||||
<script type="module" src="/components/confirm-dialog/confirm-dialog.js"></script>
|
<script type="module" src="/components/confirm-dialog/confirm-dialog.js"></script>
|
||||||
<script src="/vendor/markdown-it.min.js"></script>
|
<script src="/vendor/markdown-it.min.js"></script>
|
||||||
<script type="module" src="/components/rojo-chat-panel/rojo-chat-panel.js"></script>
|
<script type="module" src="/components/rojo-chat-panel/rojo-chat-panel.js"></script>
|
||||||
<script type="module" src="/components/rojo-settings-panel/rojo-settings-panel.js"></script>
|
|
||||||
<script src="/vendor/codemirror.min.js"></script>
|
<script src="/vendor/codemirror.min.js"></script>
|
||||||
<script src="/vendor/cm-mode-xml.min.js"></script>
|
<script src="/vendor/cm-mode-xml.min.js"></script>
|
||||||
<script src="/vendor/cm-mode-javascript.min.js"></script>
|
<script src="/vendor/cm-mode-javascript.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,15 @@
|
||||||
inkscape:deskcolor="#333333"
|
inkscape:deskcolor="#333333"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
inkscape:zoom="18.907924"
|
inkscape:zoom="3.3424804"
|
||||||
inkscape:cx="11.397338"
|
inkscape:cx="-46.821516"
|
||||||
inkscape:cy="9.6520377"
|
inkscape:cy="45.923979"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="1017"
|
inkscape:window-height="1017"
|
||||||
inkscape:window-x="-8"
|
inkscape:window-x="-8"
|
||||||
inkscape:window-y="-8"
|
inkscape:window-y="-8"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="g23" /><defs
|
inkscape:current-layer="g26" /><defs
|
||||||
id="defs2"><clipPath
|
id="defs2"><clipPath
|
||||||
clipPathUnits="userSpaceOnUse"
|
clipPathUnits="userSpaceOnUse"
|
||||||
id="clipPath7940"><rect
|
id="clipPath7940"><rect
|
||||||
|
|
@ -41,15 +41,7 @@
|
||||||
width="1440"
|
width="1440"
|
||||||
height="810"
|
height="810"
|
||||||
x="0"
|
x="0"
|
||||||
y="0" /></clipPath></defs><rect
|
y="0" /></clipPath></defs><g
|
||||||
style="fill:#9cdbf8;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
|
||||||
id="rect40"
|
|
||||||
width="31.443003"
|
|
||||||
height="30.467443"
|
|
||||||
x="-0.75042945"
|
|
||||||
y="-1.3507732"
|
|
||||||
ry="4.2382751"
|
|
||||||
inkscape:label="background" /><g
|
|
||||||
id="g26"
|
id="g26"
|
||||||
inkscape:label="root-transform"
|
inkscape:label="root-transform"
|
||||||
transform="translate(-794.40483,-571.077)"><g
|
transform="translate(-794.40483,-571.077)"><g
|
||||||
|
|
@ -132,7 +124,7 @@
|
||||||
r="0.37521484"
|
r="0.37521484"
|
||||||
inkscape:label="arm-r-pivot"
|
inkscape:label="arm-r-pivot"
|
||||||
transform="matrix(-0.78898412,0.61441359,0.61441359,0.78898412,438.93525,-341.91227)" /></g><rect
|
transform="matrix(-0.78898412,0.61441359,0.61441359,0.78898412,438.93525,-341.91227)" /></g><rect
|
||||||
style="opacity:1;fill:#1d52f5;fill-opacity:1;stroke:#390000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
style="opacity:1;fill:#949494;fill-opacity:1;stroke:#390000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||||
id="rect1"
|
id="rect1"
|
||||||
width="9.695344"
|
width="9.695344"
|
||||||
height="8.6811571"
|
height="8.6811571"
|
||||||
|
|
@ -182,7 +174,7 @@
|
||||||
style="display:inline"><g
|
style="display:inline"><g
|
||||||
id="g31"
|
id="g31"
|
||||||
inkscape:label="hair-graphics"><path
|
inkscape:label="hair-graphics"><path
|
||||||
style="opacity:1;fill:#110e16;fill-opacity:1;stroke:#005983;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
style="opacity:1;fill:#da7300;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
d="m 802.71715,590.06034 1.85722,-0.95514 c 0,0 -1.03344,-5.71606 3.3704,-5.65003 1.75987,0.0264 2.43899,1.16705 2.43899,1.16705 0,0 2.58007,-1.3539 3.74746,0.52985 1.1674,1.88375 3.01917,1.06117 3.01917,1.06117 0,0 2.22866,-5.57165 -1.0082,-8.22482 -3.23687,-2.65317 -7.69419,-2.97155 -7.69419,-2.97155 l -3.68538,2.05127 c 0,0 0.81995,-2.84722 -0.93114,-2.82069 -1.75109,0.0265 -1.88375,0.66329 -1.88375,0.66329 l 0.19026,1.39279 -1.83522,0.22564 c 0,0 -4.4308,1.1674 -1.6715,1.85722 2.75929,0.68983 3.17494,0.28828 3.17494,0.28828 l -2.19632,2.92203 c 0,0 1.56842,6.63292 2.57663,8.17176 1.0082,1.53884 0.53063,0.29185 0.53063,0.29185 z"
|
d="m 802.71715,590.06034 1.85722,-0.95514 c 0,0 -1.03344,-5.71606 3.3704,-5.65003 1.75987,0.0264 2.43899,1.16705 2.43899,1.16705 0,0 2.58007,-1.3539 3.74746,0.52985 1.1674,1.88375 3.01917,1.06117 3.01917,1.06117 0,0 2.22866,-5.57165 -1.0082,-8.22482 -3.23687,-2.65317 -7.69419,-2.97155 -7.69419,-2.97155 l -3.68538,2.05127 c 0,0 0.81995,-2.84722 -0.93114,-2.82069 -1.75109,0.0265 -1.88375,0.66329 -1.88375,0.66329 l 0.19026,1.39279 -1.83522,0.22564 c 0,0 -4.4308,1.1674 -1.6715,1.85722 2.75929,0.68983 3.17494,0.28828 3.17494,0.28828 l -2.19632,2.92203 c 0,0 1.56842,6.63292 2.57663,8.17176 1.0082,1.53884 0.53063,0.29185 0.53063,0.29185 z"
|
||||||
id="path20"
|
id="path20"
|
||||||
sodipodi:nodetypes="ccscscsccscccsccscc"
|
sodipodi:nodetypes="ccscscsccscccsccscc"
|
||||||
|
|
@ -291,7 +283,7 @@
|
||||||
cy="590.43231"
|
cy="590.43231"
|
||||||
r="0.37521484"
|
r="0.37521484"
|
||||||
inkscape:label="head-pivot" /></g><circle
|
inkscape:label="head-pivot" /></g><circle
|
||||||
style="display:inline;opacity:1;fill:#ff01e9;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
style="opacity:1;fill:#ff01e9;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
id="circle28"
|
id="circle28"
|
||||||
cx="809.2298"
|
cx="809.2298"
|
||||||
cy="600.00842"
|
cy="600.00842"
|
||||||
|
|
@ -303,42 +295,4 @@
|
||||||
cx="809.2298"
|
cx="809.2298"
|
||||||
cy="604.21301"
|
cy="604.21301"
|
||||||
r="0.37521484"
|
r="0.37521484"
|
||||||
inkscape:label="root-pivot" /></g><g
|
inkscape:label="root-pivot" /></g></svg>
|
||||||
id="g40"
|
|
||||||
transform="translate(0.712107,1.2458453)"
|
|
||||||
inkscape:label="hand-right"
|
|
||||||
style="display:inline"><path
|
|
||||||
id="path33"
|
|
||||||
style="fill:#f6e9c3;fill-opacity:1;stroke:#390000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
|
||||||
d="m 23.49461,18.354689 c -0.03984,-0.02239 -0.08255,-0.04008 -0.127809,-0.05469 -0.362105,-0.116791 -0.747713,0.08069 -0.864507,0.442792 l -0.426279,1.324715 a 2.3800299,2.2971435 14.956584 0 0 -0.111317,-0.04405 c 0.0048,-0.02587 0.0077,-0.05163 0.0096,-0.07851 l 0.112746,-1.641825 c 0.02639,-0.379557 -0.257745,-0.707525 -0.637303,-0.733913 -0.379556,-0.02639 -0.705788,0.258638 -0.732175,0.638196 l -0.114484,1.640932 c -0.0021,0.03027 -0.0019,0.06051 -1.32e-4,0.08998 a 2.3800299,2.2971435 14.956584 0 0 -0.103366,0.02592 c -0.0044,-0.06313 -0.01778,-0.127466 -0.04067,-0.19001 l -0.56532,-1.544604 c -0.130784,-0.35729 -0.523872,-0.539861 -0.881161,-0.409079 -0.357289,0.130783 -0.53986,0.523872 -0.409079,0.881161 l 0.565321,1.544605 c 0.053,0.144797 0.149432,0.262056 0.268724,0.34019 a 2.3800299,2.2971435 14.956584 0 0 -0.559577,0.981676 2.3800299,2.2971435 14.956584 0 0 1.707398,2.834494 2.3800299,2.2971435 14.956584 0 0 2.356166,-0.648815 c 0.169295,0.0374 0.353537,0.01177 0.512882,-0.08774 l 1.395186,-0.872728 c 0.322712,-0.201537 0.420038,-0.623482 0.218505,-0.946192 -0.201536,-0.322711 -0.623483,-0.420038 -0.946192,-0.218506 l -0.582348,0.363844 a 2.3800299,2.2971435 14.956584 0 0 -0.36674,-1.025255 c 0.0578,-0.06739 0.103765,-0.146921 0.132842,-0.236966 l 0.504106,-1.565809 c 0.102191,-0.316843 -0.03621,-0.652801 -0.314998,-0.809789 z" /><path
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#390000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
|
||||||
d="m 20.131882,20.281213 c 0,0 1.60243,-0.671587 2.534559,0.533693"
|
|
||||||
id="path35" /><path
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#390000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
|
||||||
d="m 23.363323,21.223444 c 0,0 0.769932,1.557567 -0.375052,2.562841"
|
|
||||||
id="path39" /></g><path
|
|
||||||
id="rect10"
|
|
||||||
style="fill:#333333;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
|
|
||||||
d="m 1.206142,-3.0986883 c -2.3480044,0 -4.2382813,1.8902768 -4.2382813,4.2382812 V 38.493108 c 0,2.348005 1.8902769,4.238281 4.2382813,4.238281 h 26.939454 c 2.348004,0 4.238281,-1.890276 4.238281,-4.238281 V 1.1395929 c 0,-2.3480044 -1.890277,-4.2382812 -4.238281,-4.2382812 z M 6.0245014,0.92084289 H 23.559658 c 2.348005,0 4.238281,1.89027691 4.238281,4.23828131 V 20.174749 c 0,2.348004 -1.890276,4.238281 -4.238281,4.238281 H 6.0245014 c -2.3480044,0 -4.2382813,-1.890277 -4.2382813,-4.238281 V 5.1591242 c 0,-2.3480044 1.8902769,-4.23828131 4.2382813,-4.23828131 z"
|
|
||||||
inkscape:label="chat-cutout" /><rect
|
|
||||||
style="fill:none;stroke:#04a2ff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;stroke-opacity:1"
|
|
||||||
id="rect2"
|
|
||||||
width="25.973324"
|
|
||||||
height="23.865683"
|
|
||||||
x="1.5883274"
|
|
||||||
y="0.60429424"
|
|
||||||
ry="4.2382751"
|
|
||||||
inkscape:label="chat-mask" /><g
|
|
||||||
id="g41"
|
|
||||||
inkscape:label="hand-left"
|
|
||||||
style="display:inline"><path
|
|
||||||
id="rect28"
|
|
||||||
style="fill:#f6e9c3;fill-opacity:1;stroke:#390000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
|
||||||
d="m 8.0469818,17.768132 c -0.044378,0.01092 -0.087629,0.02724 -0.1306365,0.04754 -0.3440199,0.162512 -0.4902027,0.570337 -0.3276935,0.914359 l 0.595253,1.257879 a 2.2971435,2.3800299 61.794592 0 0 -0.1113302,0.04402 c -0.014229,-0.02212 -0.029662,-0.04296 -0.046688,-0.06385 l -1.040872,-1.274708 c -0.240394,-0.294907 -0.6719981,-0.33977 -0.9669058,-0.09938 -0.2949086,0.240393 -0.3378934,0.671461 -0.097499,0.966369 l 1.0389951,1.275247 c 0.019172,0.02352 0.040026,0.04542 0.061442,0.06572 a 2.2971435,2.3800299 61.794592 0 0 -0.057669,0.08961 c -0.046429,-0.043 -0.1001622,-0.08081 -0.1596457,-0.110766 L 5.3347667,20.140218 c -0.3398055,-0.17115 -0.7514233,-0.03542 -0.9225746,0.304379 -0.1711507,0.339804 -0.035424,0.751423 0.3043786,0.922575 l 1.4689654,0.739954 c 0.1377124,0.06936 0.2882622,0.08893 0.4287252,0.06432 a 2.2971435,2.3800299 61.794592 0 0 0.26336,1.098842 2.2971435,2.3800299 61.794592 0 0 3.1843887,0.899575 2.2971435,2.3800299 61.794592 0 0 1.27481,-2.085027 c 0.149078,-0.08853 0.265933,-0.233259 0.31409,-0.414848 l 0.420679,-1.590982 c 0.09753,-0.367761 -0.120116,-0.742115 -0.487872,-0.839646 -0.367761,-0.09753 -0.742115,0.120117 -0.839646,0.487873 l -0.175886,0.663759 A 2.2971435,2.3800299 61.794592 0 0 9.5993372,19.894019 c -0.00394,-0.08869 -0.024815,-0.178148 -0.065202,-0.26372 L 8.830728,18.143323 C 8.6885278,17.842305 8.357757,17.691926 8.0470145,17.768123 Z" /><path
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#390000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
|
||||||
d="m 9.5310841,20.788387 c 0,0 -1.0649928,1.372807 -0.1434041,2.586165"
|
|
||||||
id="path34" /><path
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#390000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
|
||||||
d="m 9.0022053,21.846144 c 0,0 -0.126233,0.09028 -0.7780586,0.351652"
|
|
||||||
id="path40"
|
|
||||||
sodipodi:nodetypes="cc" /></g></svg>
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -1,54 +1,32 @@
|
||||||
import { ChatOpenAI } from "@langchain/openai";
|
import { ChatOpenAI } from "@langchain/openai";
|
||||||
import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from "@langchain/core/messages";
|
import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
|
||||||
|
import { RojosConfig } from "./RojosConfig";
|
||||||
|
|
||||||
export interface AgentConfig
|
const model = new ChatOpenAI({
|
||||||
{
|
model: RojosConfig.model,
|
||||||
baseURL: string;
|
apiKey: RojosConfig.apiKey,
|
||||||
model: string;
|
configuration: {
|
||||||
apiKey: string;
|
baseURL: RojosConfig.baseURL,
|
||||||
headers?: Record<string, string>;
|
},
|
||||||
systemPrompt?: string;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
type MessageRole = "user" | "assistant" | "system";
|
type MessageData = { role: "user" | "assistant"; content: string };
|
||||||
type MessageData = { role: MessageRole; content: string };
|
type SessionData = { id: string; messages: MessageData[] };
|
||||||
type SessionData = { id: string; messages: MessageData[] };
|
|
||||||
|
|
||||||
const sessions = new Map<string, SessionData>();
|
const sessions = new Map<string, SessionData>();
|
||||||
|
|
||||||
function toBaseMessages( messages: MessageData[] ): BaseMessage[]
|
function toBaseMessages( messages: MessageData[] ): BaseMessage[]
|
||||||
{
|
{
|
||||||
return messages.map( m =>
|
return messages.map( m =>
|
||||||
{
|
m.role === "user" ? new HumanMessage( m.content ) : new AIMessage( m.content )
|
||||||
if ( m.role === "user" ) return new HumanMessage( m.content );
|
);
|
||||||
if ( m.role === "assistant" ) return new AIMessage( m.content );
|
|
||||||
return new SystemMessage( m.content );
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAgentStream( id: string, userMessage: string, config: AgentConfig )
|
export async function getAgentStream( id: string, userMessage: string )
|
||||||
{
|
{
|
||||||
const model = new ChatOpenAI(
|
if ( ! sessions.has( id ) )
|
||||||
{
|
{
|
||||||
model: config.model,
|
sessions.set( id, { id, messages: [] } );
|
||||||
apiKey: config.apiKey,
|
|
||||||
configuration:
|
|
||||||
{
|
|
||||||
baseURL: config.baseURL,
|
|
||||||
defaultHeaders: config.headers ?? {},
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
|
|
||||||
if ( !sessions.has( id ) )
|
|
||||||
{
|
|
||||||
const messages: MessageData[] = [];
|
|
||||||
|
|
||||||
if ( config.systemPrompt?.trim() )
|
|
||||||
{
|
|
||||||
messages.push( { role: "system", content: config.systemPrompt } );
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions.set( id, { id, messages } );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = sessions.get( id )!;
|
const session = sessions.get( id )!;
|
||||||
|
|
@ -57,7 +35,7 @@ export async function getAgentStream( id: string, userMessage: string, config: A
|
||||||
return model.stream( toBaseMessages( session.messages ) );
|
return model.stream( toBaseMessages( session.messages ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAgentConversation( id: string, assistantMessage: string ): void
|
export function updateAgentConversation( id: string, assistantMessage: string )
|
||||||
{
|
{
|
||||||
const session = sessions.get( id );
|
const session = sessions.get( id );
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,202 +1,16 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import fs from "fs";
|
import { requireAuth } from "../middleware/auth";
|
||||||
import path from "path";
|
import { getAgentStream, updateAgentConversation } from "../rojos/RojosAgent";
|
||||||
import crypto from "crypto";
|
import { RJLog } from "../../library-ts/node/log/RJLog";
|
||||||
import { requireAuth } from "../middleware/auth";
|
|
||||||
import { getAgentStream, updateAgentConversation, AgentConfig } from "../rojos/RojosAgent";
|
|
||||||
import { readProjectFile, writeProjectFile, createProjectDirectory } from "../storage";
|
|
||||||
import { checkAccess } from "../projectAccess";
|
|
||||||
import { RojosConfig } from "../rojos/RojosConfig";
|
|
||||||
import { ROOT } from "../rootDir";
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
const STORAGE = path.join( ROOT, "build", "data", "storage" );
|
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
router.use( requireAuth );
|
router.use( requireAuth );
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function normalizeUrl( url: string ): string
|
|
||||||
{
|
|
||||||
if ( !url.startsWith( "http://" ) && !url.startsWith( "https://" ) )
|
|
||||||
{
|
|
||||||
return "http://" + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pick<T>( arr: T[] ): T
|
|
||||||
{
|
|
||||||
return arr[ Math.floor( Math.random() * arr.length ) ];
|
|
||||||
}
|
|
||||||
|
|
||||||
function cap( s: string ): string { return s[ 0 ].toUpperCase() + s.slice( 1 ); }
|
|
||||||
|
|
||||||
const ADJECTIVES = [
|
|
||||||
"funky", "cool", "groovy", "wild", "sharp", "slick", "swift", "bold",
|
|
||||||
"bright", "calm", "daring", "eager", "fierce", "jolly", "lively",
|
|
||||||
"quirky", "smart", "vivid", "zesty", "sleek", "crisp", "nifty", "rad",
|
|
||||||
"sunny", "sassy", "snappy", "peppy", "zippy", "breezy", "mellow",
|
|
||||||
];
|
|
||||||
|
|
||||||
const ROLES = [
|
|
||||||
"boss", "doctor", "player", "teacher", "hunter", "builder", "maker",
|
|
||||||
"dancer", "singer", "painter", "writer", "coder", "pilot", "chef",
|
|
||||||
"sailor", "rider", "climber", "dreamer", "wanderer", "scout", "keeper",
|
|
||||||
"helper", "guide", "mentor", "runner", "seeker", "ranger", "scholar",
|
|
||||||
];
|
|
||||||
|
|
||||||
// 50% European, 50% rest of world — modern names, male/female mixed
|
|
||||||
const NAMES = [
|
|
||||||
"Sofia", "Elena", "Lucas", "Mia", "Noah", "Emma", "Leon", "Lena",
|
|
||||||
"Felix", "Anna", "Max", "Clara", "Julian", "Sara", "Lars", "Nina",
|
|
||||||
"Tom", "Lara", "Erik", "Ida", "Hugo", "Vera", "Otto", "Maja",
|
|
||||||
"Kai", "Maya", "Arjun", "Zara", "Aisha", "Omar", "Nala", "Ravi",
|
|
||||||
"Yuki", "Kenji", "Amara", "Diego", "Mateo", "Jae", "Sora", "Kira",
|
|
||||||
"Rio", "Zion", "Nova", "Leila", "Cyrus", "Noa", "Bao", "Mila",
|
|
||||||
];
|
|
||||||
|
|
||||||
function generateRojoName(): { display: string; slug: string }
|
|
||||||
{
|
|
||||||
const adj = pick( ADJECTIVES );
|
|
||||||
const role = pick( ROLES );
|
|
||||||
const name = pick( NAMES );
|
|
||||||
return {
|
|
||||||
display: `${ cap( adj ) } ${ cap( role ) } ${ name }`,
|
|
||||||
slug: `${ adj }-${ role }-${ name.toLowerCase() }`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Recursive *.rojo scanner ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function scanRojoFiles( dir: string, projectRoot: string, out: string[] = [] ): string[]
|
|
||||||
{
|
|
||||||
if ( !fs.existsSync( dir ) ) return out;
|
|
||||||
|
|
||||||
for ( const entry of fs.readdirSync( dir, { withFileTypes: true } ) )
|
|
||||||
{
|
|
||||||
const full = path.join( dir, entry.name );
|
|
||||||
|
|
||||||
if ( entry.isDirectory() )
|
|
||||||
{
|
|
||||||
scanRojoFiles( full, projectRoot, out );
|
|
||||||
}
|
|
||||||
else if ( entry.name.endsWith( ".rojo" ) )
|
|
||||||
{
|
|
||||||
out.push( path.relative( projectRoot, full ).replace( /\\/g, "/" ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tunnel browser (proxies to tunnel.rokojori.com) ─────────────────────────
|
|
||||||
|
|
||||||
router.get( "/tunnels/browse", async ( req, res ) =>
|
|
||||||
{
|
|
||||||
const purpose = typeof req.query.purpose === "string" ? req.query.purpose : "";
|
|
||||||
|
|
||||||
const token = req.cookies?.accessToken
|
|
||||||
|| ( typeof req.headers.authorization === "string" && req.headers.authorization.startsWith( "Bearer " )
|
|
||||||
? req.headers.authorization.slice( 7 )
|
|
||||||
: "" );
|
|
||||||
|
|
||||||
if ( !token ) { res.status( 401 ).json( { error: "No auth token" } ); return; }
|
|
||||||
|
|
||||||
const tunnelServer = process.env.TUNNEL_SERVER_URL || "https://tunnel.rokojori.com";
|
|
||||||
const url = purpose
|
|
||||||
? `${ tunnelServer }/api/tunnels/available?purpose=${ encodeURIComponent( purpose ) }`
|
|
||||||
: `${ tunnelServer }/api/tunnels/available`;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const upstream = await fetch( url, { headers: { Authorization: `Bearer ${ token }` } } );
|
|
||||||
const data = await upstream.json();
|
|
||||||
res.json( data );
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
res.status( 502 ).json( { error: "Could not reach tunnel server" } );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// ── List rojos ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
router.get( "/:projectId/list", ( req, res ) =>
|
|
||||||
{
|
|
||||||
const denied = checkAccess( req.params.projectId, req.user!, "view" );
|
|
||||||
if ( denied ) { res.status( denied.status ).json( { error: denied.error } ); return; }
|
|
||||||
|
|
||||||
const projectRoot = path.join( STORAGE, req.params.projectId, "root" );
|
|
||||||
const rojoDir = path.join( projectRoot, "workspace", "rojos" );
|
|
||||||
const files = scanRojoFiles( rojoDir, projectRoot );
|
|
||||||
|
|
||||||
const rojos = files.map( filePath =>
|
|
||||||
{
|
|
||||||
let id = "", name = filePath, description = "";
|
|
||||||
|
|
||||||
const content = readProjectFile( req.params.projectId, filePath );
|
|
||||||
|
|
||||||
if ( content )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const s = JSON.parse( content );
|
|
||||||
id = s.id || "";
|
|
||||||
name = s.name || name;
|
|
||||||
description = s.description || "";
|
|
||||||
}
|
|
||||||
catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { id, name, description, path: filePath };
|
|
||||||
} );
|
|
||||||
|
|
||||||
res.json( rojos );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// ── Create rojo ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
router.post( "/:projectId/create", ( req, res ) =>
|
|
||||||
{
|
|
||||||
const denied = checkAccess( req.params.projectId, req.user!, "edit" );
|
|
||||||
if ( denied ) { res.status( denied.status ).json( { error: denied.error } ); return; }
|
|
||||||
|
|
||||||
const { parentDir } = req.body as { parentDir?: string };
|
|
||||||
const dir = parentDir || "workspace/rojos";
|
|
||||||
const { display, slug } = generateRojoName();
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const filePath = `${ dir }/${ slug }.rojo`;
|
|
||||||
|
|
||||||
const defaults = JSON.stringify(
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
name: display,
|
|
||||||
description: "",
|
|
||||||
systemPrompt: "",
|
|
||||||
endpoint: { type: "external", url: "", model: "", apiKey: "", tunnelId: "" },
|
|
||||||
appearance: { colors: [], layers: [] },
|
|
||||||
}, null, 2 );
|
|
||||||
|
|
||||||
const ok = writeProjectFile( req.params.projectId, filePath, defaults );
|
|
||||||
|
|
||||||
if ( !ok ) { res.status( 500 ).json( { error: "Could not create rojo file" } ); return; }
|
|
||||||
|
|
||||||
res.json( { id, path: filePath, name: display } );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// ── Chat ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
router.post( "/chat", async ( req, res ) =>
|
router.post( "/chat", async ( req, res ) =>
|
||||||
{
|
{
|
||||||
const { id, message, projectId, rojoPath } = req.body as
|
const { id, message } = req.body as { id: string; message: string };
|
||||||
{
|
|
||||||
id: string;
|
// RJLog.log( "Chat", { id, message } );
|
||||||
message: string;
|
|
||||||
projectId?: string;
|
|
||||||
rojoPath?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( !id || !message )
|
if ( !id || !message )
|
||||||
{
|
{
|
||||||
|
|
@ -204,57 +18,13 @@ router.post( "/chat", async ( req, res ) =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: AgentConfig = {
|
|
||||||
baseURL: RojosConfig.baseURL,
|
|
||||||
model: RojosConfig.model,
|
|
||||||
apiKey: RojosConfig.apiKey,
|
|
||||||
systemPrompt: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( projectId && rojoPath )
|
|
||||||
{
|
|
||||||
const content = readProjectFile( projectId, rojoPath );
|
|
||||||
|
|
||||||
if ( content )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const settings = JSON.parse( content );
|
|
||||||
const ep = settings.endpoint ?? {};
|
|
||||||
|
|
||||||
config.systemPrompt = settings.systemPrompt ?? "";
|
|
||||||
|
|
||||||
if ( ep.type === "tunnel" && ep.tunnelId )
|
|
||||||
{
|
|
||||||
const tunnelServer = process.env.TUNNEL_SERVER_URL || "https://tunnel.rokojori.com";
|
|
||||||
config.baseURL = `${ tunnelServer }/t/${ ep.tunnelId }/v1`;
|
|
||||||
config.model = ep.model || RojosConfig.model;
|
|
||||||
config.apiKey = "not-needed";
|
|
||||||
|
|
||||||
const token = req.cookies?.accessToken
|
|
||||||
|| ( typeof req.headers.authorization === "string" && req.headers.authorization.startsWith( "Bearer " )
|
|
||||||
? req.headers.authorization.slice( 7 )
|
|
||||||
: "" );
|
|
||||||
if ( token ) config.headers = { Authorization: `Bearer ${ token }` };
|
|
||||||
}
|
|
||||||
else if ( ep.url )
|
|
||||||
{
|
|
||||||
config.baseURL = normalizeUrl( ep.url );
|
|
||||||
config.model = ep.model || RojosConfig.model;
|
|
||||||
config.apiKey = ep.apiKey || "not-needed";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const agentStream = await getAgentStream( id, message, config );
|
const agentStream = await getAgentStream( id, message );
|
||||||
|
|
||||||
res.setHeader( "Content-Type", "text/event-stream" );
|
res.setHeader( "Content-Type", "text/event-stream" );
|
||||||
res.setHeader( "Cache-Control", "no-cache" );
|
res.setHeader( "Cache-Control", "no-cache" );
|
||||||
res.setHeader( "Connection", "keep-alive" );
|
res.setHeader( "Connection", "keep-alive" );
|
||||||
res.flushHeaders();
|
res.flushHeaders();
|
||||||
|
|
||||||
const collected: string[] = [];
|
const collected: string[] = [];
|
||||||
|
|
@ -264,6 +34,8 @@ router.post( "/chat", async ( req, res ) =>
|
||||||
const content = chunk.content;
|
const content = chunk.content;
|
||||||
let text = "";
|
let text = "";
|
||||||
|
|
||||||
|
// RJLog.log( "Chunk", chunk );
|
||||||
|
|
||||||
if ( typeof content === "string" )
|
if ( typeof content === "string" )
|
||||||
{
|
{
|
||||||
text = content;
|
text = content;
|
||||||
|
|
@ -272,8 +44,14 @@ router.post( "/chat", async ( req, res ) =>
|
||||||
{
|
{
|
||||||
for ( const c of content )
|
for ( const c of content )
|
||||||
{
|
{
|
||||||
if ( typeof c === "string" ) text += c;
|
if ( typeof c === "string" )
|
||||||
else if ( c && typeof c === "object" && "text" in c ) text += ( c as any ).text;
|
{
|
||||||
|
text += c;
|
||||||
|
}
|
||||||
|
else if ( c && typeof c === "object" && "text" in c )
|
||||||
|
{
|
||||||
|
text += ( c as any ).text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,13 +67,18 @@ router.post( "/chat", async ( req, res ) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAgentConversation( id, collected.join( "" ) );
|
updateAgentConversation( id, collected.join( "" ) );
|
||||||
|
|
||||||
res.write( JSON.stringify( { type: "DONE" } ) + "\n" );
|
res.write( JSON.stringify( { type: "DONE" } ) + "\n" );
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
catch ( err )
|
catch ( err )
|
||||||
{
|
{
|
||||||
console.error( err );
|
console.error( err );
|
||||||
if ( !res.headersSent ) res.status( 500 ).json( { error: "Failed to get agent response" } );
|
|
||||||
|
if ( !res.headersSent )
|
||||||
|
{
|
||||||
|
res.status( 500 ).json( { error: "Failed to get agent response" } );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,15 @@
|
||||||
inkscape:deskcolor="#333333"
|
inkscape:deskcolor="#333333"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
inkscape:zoom="0.59087265"
|
inkscape:zoom="0.41781006"
|
||||||
inkscape:cx="563.57321"
|
inkscape:cx="-1493.5016"
|
||||||
inkscape:cy="733.66063"
|
inkscape:cy="3112.6584"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="1017"
|
inkscape:window-height="1017"
|
||||||
inkscape:window-x="-8"
|
inkscape:window-x="-8"
|
||||||
inkscape:window-y="-8"
|
inkscape:window-y="-8"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1" /><defs
|
inkscape:current-layer="g101" /><defs
|
||||||
id="defs2"><linearGradient
|
id="defs2"><linearGradient
|
||||||
id="linearGradient99"
|
id="linearGradient99"
|
||||||
inkscape:collect="never"><stop
|
inkscape:collect="never"><stop
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
|
@ -22,58 +22,6 @@
|
||||||
<div class="lane">
|
<div class="lane">
|
||||||
<div class="lane-header">To Do</div>
|
<div class="lane-header">To Do</div>
|
||||||
|
|
||||||
<task-item class="blue hide-content">
|
|
||||||
<task-title>Rojo Character Editor</task-title>
|
|
||||||
<task-content>
|
|
||||||
Allow changing colors and selecting layers of a Rojo character from within
|
|
||||||
the rojo-settings-panel. Colors map to fill/stroke/both targets on SVG elements;
|
|
||||||
layers toggle visibility of named groups or swap between variants.
|
|
||||||
The appearance field (colors[], layers[]) is already in the settings.rojo schema.
|
|
||||||
</task-content>
|
|
||||||
</task-item>
|
|
||||||
|
|
||||||
<task-item class="blue hide-content">
|
|
||||||
<task-title>Rojo Character Animation Box</task-title>
|
|
||||||
<task-content>
|
|
||||||
Animation system for emotional feedback during conversations.
|
|
||||||
Animations can be scripted (predefined sequences) or driven dynamically by
|
|
||||||
an LLM that emits emotion tags alongside its response. The animation box
|
|
||||||
plays character animations (idle, happy, thinking, surprised, etc.) in the
|
|
||||||
portrait area of the rojo-settings-panel and rojo-chat-panel.
|
|
||||||
</task-content>
|
|
||||||
</task-item>
|
|
||||||
|
|
||||||
<task-item class="blue hide-content">
|
|
||||||
<task-title>File tree: drag-and-drop move for files and directories</task-title>
|
|
||||||
<task-content>
|
|
||||||
Allow files and directories to be moved by dragging them within the file tree.
|
|
||||||
Dragging a file onto a directory moves it inside; dragging a directory onto another
|
|
||||||
directory moves the whole subtree. Use the existing file rename API
|
|
||||||
(POST /api/files/:projectId/rename) — moving is a rename to a new parent path.
|
|
||||||
</task-content>
|
|
||||||
</task-item>
|
|
||||||
|
|
||||||
<task-item class="blue hide-content">
|
|
||||||
<task-title>Tab context menu on right-click (tabs and empty tab bar area)</task-title>
|
|
||||||
<task-content>
|
|
||||||
The tab ⋮ menu already works but should also open on right-click anywhere on
|
|
||||||
the tab bar — both on individual tabs and on the empty space to the right of the tabs.
|
|
||||||
Right-clicking a specific tab should also offer a "Close this tab" action directly.
|
|
||||||
</task-content>
|
|
||||||
</task-item>
|
|
||||||
|
|
||||||
<task-item class="blue hide-content">
|
|
||||||
<task-title>File tree double-click: auto-open or focus existing editor</task-title>
|
|
||||||
<task-content>
|
|
||||||
When a file is double-clicked in the file tree:
|
|
||||||
— If an editor panel that can handle the file type is already open and not pinned,
|
|
||||||
focus that panel's tab and load the file into it.
|
|
||||||
— If no suitable unpinned editor exists, open a new panel of the correct type
|
|
||||||
in the active section before loading the file.
|
|
||||||
Single-click keeps current behaviour (selection only, no open).
|
|
||||||
</task-content>
|
|
||||||
</task-item>
|
|
||||||
|
|
||||||
<task-item class="blue hide-content">
|
<task-item class="blue hide-content">
|
||||||
<task-title>Remote Projects in Electron</task-title>
|
<task-title>Remote Projects in Electron</task-title>
|
||||||
<task-content>
|
<task-content>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue