Changeset 0.22.4 (#146)
This commit is contained in:
@@ -259,6 +259,8 @@ const API = {
|
||||
getWorkspaceGitBranches(wsId) { return this._get(`/api/v1/workspaces/${wsId}/git/branches`); },
|
||||
listWorkspaces() { return this._get('/api/v1/workspaces'); },
|
||||
createWorkspace(data) { return this._post('/api/v1/workspaces', data); },
|
||||
updateWorkspace(id, patch) { return this._patch(`/api/v1/workspaces/${id}`, patch); },
|
||||
listGitCredentials() { return this._get('/api/v1/git-credentials'); },
|
||||
|
||||
// ── Messages ─────────────────────────────
|
||||
|
||||
@@ -727,6 +729,33 @@ const API = {
|
||||
adminListCapabilityOverrides() { return this._get('/api/v1/admin/capability-overrides'); },
|
||||
adminGetProviderTypes() { return this._get('/api/v1/admin/provider-types'); },
|
||||
|
||||
// ── Project Files (v0.22.4) ─────────────
|
||||
async projectUploadFile(projectId, file) {
|
||||
const form = new FormData();
|
||||
form.append('file', file, file.name);
|
||||
const doFetch = () => fetch(BASE + `/api/v1/projects/${projectId}/files`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${this.accessToken}` },
|
||||
body: form,
|
||||
});
|
||||
let resp = await doFetch();
|
||||
if (resp.status === 401 && this.refreshToken) {
|
||||
await this._refreshAccessToken();
|
||||
resp = await doFetch();
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
return resp.json();
|
||||
},
|
||||
projectListFiles(projectId) { return this._get(`/api/v1/projects/${projectId}/files`); },
|
||||
|
||||
// ── Export (v0.22.4) ────────────────────
|
||||
exportDocument(content, format, filename) {
|
||||
return this._post('/api/v1/export', { content, format, filename });
|
||||
},
|
||||
|
||||
// ── User Usage ──────────────────────────
|
||||
getMyUsage(params) {
|
||||
const q = new URLSearchParams();
|
||||
|
||||
@@ -333,6 +333,8 @@ const EditorMode = {
|
||||
<div class="editor-export-menu" id="editorExportMenu">
|
||||
<button class="editor-export-item" data-format="md">Download file</button>
|
||||
<button class="editor-export-item" data-format="html">Export as HTML</button>
|
||||
<button class="editor-export-item" data-format="pdf">Export as PDF</button>
|
||||
<button class="editor-export-item" data-format="docx">Export as DOCX</button>
|
||||
<button class="editor-export-item" data-format="clipboard">Copy to clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1020,6 +1022,37 @@ pre{background:#f5f5f5;padding:16px;border-radius:6px;overflow-x:auto}code{font-
|
||||
a.click(); URL.revokeObjectURL(url);
|
||||
break;
|
||||
}
|
||||
case 'pdf':
|
||||
case 'docx': {
|
||||
try {
|
||||
if (typeof UI !== 'undefined') UI.toast(`Exporting as ${format.toUpperCase()}…`, 'info');
|
||||
const resp = await fetch(
|
||||
(window.BASE_PATH || '') + '/api/v1/export',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({ content, format, filename: fileName }),
|
||||
}
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
const blob = await resp.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName.replace(/\.[^.]+$/, '.' + format);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
if (typeof UI !== 'undefined') UI.toast(`Export failed: ${e.message}`, 'error');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'clipboard': {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
|
||||
@@ -409,6 +409,26 @@ function _registerProjectPanel() {
|
||||
</div>
|
||||
<div class="project-panel-hint">Workspace files are available to AI in all project chats. Enables the Editor surface.</div>
|
||||
</div>
|
||||
<div class="project-panel-section" id="projectGitSection" style="display:none">
|
||||
<label class="project-panel-label">Git Settings</label>
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
<input type="text" id="projectGitRemote" class="project-panel-input" placeholder="Git remote URL (https://...)" spellcheck="false">
|
||||
<input type="text" id="projectGitBranch" class="project-panel-input" placeholder="Branch (default: main)" spellcheck="false">
|
||||
<select id="projectGitCredential" class="project-panel-select">
|
||||
<option value="">No credential</option>
|
||||
</select>
|
||||
<button class="btn-small" id="projectGitSave">Save Git Config</button>
|
||||
</div>
|
||||
<div class="project-panel-hint">Configure git remote to sync workspace files with a repository.</div>
|
||||
</div>
|
||||
<div class="project-panel-section" id="projectFilesSection">
|
||||
<label class="project-panel-label">Project Files</label>
|
||||
<div id="projectFileList" class="project-panel-list"></div>
|
||||
<div style="margin-top:6px">
|
||||
<input type="file" id="projectFileInput" style="display:none" multiple>
|
||||
<button class="btn-small" id="projectFileUploadBtn">Upload File</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-panel-section project-panel-footer">
|
||||
<label class="project-panel-checkbox">
|
||||
<input type="checkbox" id="projectArchiveToggle">
|
||||
@@ -434,6 +454,15 @@ function _registerProjectPanel() {
|
||||
el.querySelector('#projectWsSelect').addEventListener('change', _saveProjectWorkspace);
|
||||
el.querySelector('#projectWsCreate').addEventListener('click', _createProjectWorkspace);
|
||||
|
||||
// Wire git settings (v0.22.4)
|
||||
el.querySelector('#projectGitSave').addEventListener('click', _saveProjectGitConfig);
|
||||
|
||||
// Wire project files (v0.22.4)
|
||||
el.querySelector('#projectFileUploadBtn').addEventListener('click', () => {
|
||||
document.getElementById('projectFileInput').click();
|
||||
});
|
||||
el.querySelector('#projectFileInput').addEventListener('change', _handleProjectFileUpload);
|
||||
|
||||
PanelRegistry.register('project', {
|
||||
element: el,
|
||||
label: 'Project',
|
||||
@@ -481,6 +510,12 @@ async function _loadProjectPanel(projectId) {
|
||||
|
||||
// Load workspace picker (v0.21.5)
|
||||
await _renderProjectWorkspace(projectId);
|
||||
|
||||
// Load git settings (v0.22.4) — depends on workspace being loaded
|
||||
await _renderProjectGitSettings(projectId);
|
||||
|
||||
// Load project files (v0.22.4)
|
||||
await _renderProjectFiles(projectId);
|
||||
}
|
||||
|
||||
async function _saveProjectPrompt() {
|
||||
@@ -700,6 +735,8 @@ async function _saveProjectWorkspace() {
|
||||
UI.toast(wsId ? 'Workspace linked' : 'Workspace unlinked', 'success');
|
||||
// Trigger editor surface check
|
||||
if (typeof EditorMode !== 'undefined') EditorMode.check();
|
||||
// Refresh git settings (v0.22.4)
|
||||
await _renderProjectGitSettings(_projectPanelId);
|
||||
} catch (e) {
|
||||
UI.toast('Failed to update workspace', 'error');
|
||||
}
|
||||
@@ -731,6 +768,106 @@ async function _createProjectWorkspace() {
|
||||
|
||||
// ── Channel workspace picker (v0.21.5) ───────
|
||||
|
||||
// ── Workspace Git Config (v0.22.4) ───────────
|
||||
|
||||
async function _renderProjectGitSettings(projectId) {
|
||||
const gitSection = document.getElementById('projectGitSection');
|
||||
const wsId = document.getElementById('projectWsSelect')?.value;
|
||||
if (!wsId) {
|
||||
gitSection.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
gitSection.style.display = '';
|
||||
|
||||
try {
|
||||
const ws = await API.getWorkspace(wsId);
|
||||
document.getElementById('projectGitRemote').value = ws.git_remote_url || '';
|
||||
document.getElementById('projectGitBranch').value = ws.git_branch || '';
|
||||
|
||||
// Load git credentials
|
||||
const credSel = document.getElementById('projectGitCredential');
|
||||
credSel.innerHTML = '<option value="">No credential</option>';
|
||||
try {
|
||||
const creds = await API.listGitCredentials();
|
||||
const list = Array.isArray(creds) ? creds : (creds?.data || []);
|
||||
for (const c of list) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c.id;
|
||||
opt.textContent = c.name || c.id.slice(0, 8);
|
||||
if (c.id === ws.git_credential_id) opt.selected = true;
|
||||
credSel.appendChild(opt);
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
gitSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function _saveProjectGitConfig() {
|
||||
const wsId = document.getElementById('projectWsSelect')?.value;
|
||||
if (!wsId) return;
|
||||
|
||||
const remote = document.getElementById('projectGitRemote').value.trim();
|
||||
const branch = document.getElementById('projectGitBranch').value.trim() || null;
|
||||
const credId = document.getElementById('projectGitCredential').value || null;
|
||||
|
||||
try {
|
||||
await API.updateWorkspace(wsId, {
|
||||
git_remote_url: remote || null,
|
||||
git_branch: branch,
|
||||
git_credential_id: credId,
|
||||
});
|
||||
UI.toast('Git config saved', 'success');
|
||||
} catch (e) {
|
||||
UI.toast('Failed to save git config: ' + (e.message || e), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Project Files (v0.22.4) ──────────────────
|
||||
|
||||
async function _renderProjectFiles(projectId) {
|
||||
const list = document.getElementById('projectFileList');
|
||||
if (!list) return;
|
||||
|
||||
try {
|
||||
const resp = await API.projectListFiles(projectId);
|
||||
const files = resp?.files || [];
|
||||
if (!files.length) {
|
||||
list.innerHTML = '<div class="project-panel-empty">No files uploaded</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = files.map(f => {
|
||||
const size = f.size_bytes > 1024 * 1024
|
||||
? (f.size_bytes / 1024 / 1024).toFixed(1) + ' MB'
|
||||
: (f.size_bytes / 1024).toFixed(1) + ' KB';
|
||||
return `<div class="project-panel-item" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span title="${esc(f.filename)}">${esc(f.filename)}</span>
|
||||
<span class="text-muted" style="font-size:11px">${size}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
list.innerHTML = '<div class="project-panel-empty">Failed to load files</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function _handleProjectFileUpload() {
|
||||
const input = document.getElementById('projectFileInput');
|
||||
if (!input.files?.length || !_projectPanelId) return;
|
||||
|
||||
for (const file of input.files) {
|
||||
try {
|
||||
await API.projectUploadFile(_projectPanelId, file);
|
||||
UI.toast(`Uploaded ${file.name}`, 'success');
|
||||
} catch (e) {
|
||||
UI.toast(`Upload failed: ${e.message || e}`, 'error');
|
||||
}
|
||||
}
|
||||
input.value = '';
|
||||
await _renderProjectFiles(_projectPanelId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function showWorkspacePicker(chatId) {
|
||||
let workspaces = [];
|
||||
try {
|
||||
|
||||
@@ -929,6 +929,10 @@ Object.assign(UI, {
|
||||
<span class="admin-storage-label">Timeouts</span>
|
||||
<span class="admin-storage-value">${p.timeout_count ?? 0}</span>
|
||||
</div>
|
||||
<div class="admin-storage-item">
|
||||
<span class="admin-storage-label">Rate Limits</span>
|
||||
<span class="admin-storage-value" style="color:${(p.rate_limit_count || 0) > 0 ? 'var(--warning)' : 'inherit'}">${p.rate_limit_count ?? 0}</span>
|
||||
</div>
|
||||
<div class="admin-storage-item">
|
||||
<span class="admin-storage-label">Max Latency</span>
|
||||
<span class="admin-storage-value">${p.max_latency_ms ?? 0}ms</span>
|
||||
|
||||
Reference in New Issue
Block a user