Major UI work (and also add update backend endpoints to accomadate)

This commit is contained in:
Saood Karim
2025-08-10 23:04:20 -05:00
parent 276c045496
commit be74226840
2 changed files with 761 additions and 11 deletions

View File

@@ -1953,15 +1953,16 @@ function Sessions({ sessionStorage, disabled }) {
};
const exportAll = async () => {
alert("Warning: This can take a lot of time and space. Leave the page now if you do not want to proceed.")
const db = await sessionStorage.openDatabase();
const sessionKeys = Object.keys(sessionStorage.sessions);
for (const sessionKey of sessionKeys) {
const processedSession = await sessionStorage.loadFromDatabase(db, sessionKey);
for (const [key, value] of Object.entries(processedSession)) {
processedSession[key] = JSON.stringify(value);
if (confirm("Warning: This can take a lot of time and space. Be patient if you proceed.")) {
const db = await sessionStorage.openDatabase();
const sessionKeys = Object.keys(sessionStorage.sessions);
for (const sessionKey of sessionKeys) {
const processedSession = await sessionStorage.loadFromDatabase(db, sessionKey);
for (const [key, value] of Object.entries(processedSession)) {
processedSession[key] = JSON.stringify(value);
}
exportText(`${processedSession.name}.json`, JSON.stringify(processedSession));
}
exportText(`${processedSession.name}.json`, JSON.stringify(processedSession));
}
};
@@ -2096,7 +2097,7 @@ function Sessions({ sessionStorage, disabled }) {
</li>
`)}
</ul>
<div className="vbox">
<div className="vbox" style="${{ 'grid-template-rows':'0fr 0fr 0fr 0fr 0fr' }}">
<button disabled=${disabled} onClick=${startCreateSession}>Create</button>
<button disabled=${disabled} onClick=${importSession}>Import</button>
<button disabled=${disabled} onClick=${exportSession}>Export</button>
@@ -2424,6 +2425,66 @@ const SVG_Moveable = ({...props}) => {
</${SVG}>
`};
const SVG_SortIndicator = ({ sortOrder, ...props }) => {
return html`
<${SVG}
...${props}
width="8"
height="8"
viewBox="0 0 8 8"
style=${{ 'margin-left': '2px' }}>
${sortOrder === 'asc'
? html`<path d="M0 4 L4 0 L8 4 Z"/>`
: html`<path d="M0 0 L4 4 L8 0 Z"/>`}
</${SVG}>
`};
const SVG_SortName = ({...props}) => {
return html`
<${SVG}
...${props}
width="16"
height="16"
viewBox="0 0 16 16">
<path d="M2 12L5 4h2l3 8H8.2L7.5 10H4.5L3.8 12H2zM4.7 8h2.1L5.8 5.5 4.7 8z"/>
<path d="M9 4h5v1H10.4l3.6 6H9v-1h3.6L9 5V4z"/>
</${SVG}>
`};
const SVG_SortTokens = ({...props}) => {
return html`
<${SVG}
...${props}
width="16"
height="16"
viewBox="0 0 16 16">
<path d="M3 5h2v2H3zm4 0h2v2H7zm4 0h2v2h-2zM3 9h2v2H3zm4 0h2v2H7z"/>
</${SVG}>
`};
const SVG_SortSize = ({...props}) => {
return html`
<${SVG}
...${props}
width="16"
height="16"
viewBox="0 0 16 16">
<path d="M2 2h12v3H2zm0 5h10v3H2zm0 5h8v3H2z"/>
</${SVG}>
`};
const SVG_SortDate = ({...props}) => {
return html`
<${SVG}
...${props}
width="16"
height="16"
viewBox="0 0 16 16">
<path d="M13 2h-1V1h-2v1H6V1H4v1H3c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zM13 13H3V5h10v8z"/>
</${SVG}>
`};
function Modal({ isOpen, onClose, title, description, children, ...props }) {
if (!isOpen) {
return null;
@@ -3725,6 +3786,423 @@ function InstructModal({ isOpen, closeModal, predict, cancel, modalState, templa
`;
}
function SavedPromptsModal({ isOpen, closeModal, cancel }) {
const [savedPrompts, setSavedPrompts] = useState([]);
const [simplePrompts, setSimplePrompts] = useState([]);
const [selectedPrompt, setSelectedPrompt] = useState(null);
const [loading, setLoading] = useState(false);
const [saveName, setSaveName] = useState('');
const [loadSlot, setLoadSlot] = useState('');
const [activeTab, setActiveTab] = useState('saved'); // 'saved' or 'simple'
const [sortBy, setSortBy] = useState('name'); // 'name', 'tokens', 'size', 'date'
const [sortOrder, setSortOrder] = useState('asc'); // 'asc' or 'desc'
const [listWidth, setListWidth] = useState(300);
const [modalHeight, setModalHeight] = useState(500);
const [renameNewName, setRenameNewName] = useState('');
const [renameOldName, setRenameOldName] = useState('');
// Fetch data on modal open
useEffect(() => {
if (isOpen) {
fetchSavedPrompts();
fetchSimplePrompts();
}
}, [isOpen]);
const fetchSavedPrompts = async () => {
setLoading(true);
try {
const res = await fetch('/list');
const data = await res.json();
setSavedPrompts(data);
} catch (error) {
console.error('Failed to fetch saved prompts:', error);
}
setLoading(false);
};
const fetchSimplePrompts = async () => {
try {
const res = await fetch('/slots/list');
const data = await res.json();
setSimplePrompts(data);
} catch (error) {
console.error('Failed to fetch simple prompts:', error);
}
};
const handleSort = (type) => {
if (sortBy === type) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortBy(type);
setSortOrder('asc');
}
};
const sortedPrompts = [...savedPrompts].sort((a, b) => {
let compareValue = 0;
switch (sortBy) {
case 'name':
compareValue = a.filename.localeCompare(b.filename);
break;
case 'tokens':
compareValue = a.token_count - b.token_count;
break;
case 'size':
compareValue = a.filesize - b.filesize;
break;
case 'date':
compareValue = (new Date(a.mtime))- (new Date(b.mtime));
break;
}
return sortOrder === 'asc' ? compareValue : -compareValue;
});
const handleSavePrompt = async (slot_id) => {
if (!saveName.trim()) {
alert('Please enter a name for the saved prompt');
return;
}
try {
await fetch('/slots/' + slot_id + '?action=save', {
method: 'POST',
body: JSON.stringify({
filename: `${saveName}.bin`,
})
});
setSaveName('');
fetchSavedPrompts();
} catch (error) {
console.error('Failed to save prompt:', error);
}
};
const handleLoadPrompt = async (filename) => {
if (loadSlot === '') {
alert('Please select a slot to restore to');
return;
}
if (confirm(`Load "${filename}" into slot "${loadSlot}"? This will overwrite what is currently there.`)) {
try {
await fetch('/slots/' + loadSlot + '?action=restore', {
method: 'POST',
body: JSON.stringify({ filename })
});
fetchSimplePrompts();
} catch (error) {
console.error('Failed to load prompt:', error);
}
}
};
const handleDeletePrompt = async (filename) => {
if (confirm(`Delete "${filename}"? This cannot be undone.`)) {
try {
await fetch('/delete_prompt', {
method: 'POST',
body: JSON.stringify({ filename })
});
setSelectedPrompt(null);
fetchSavedPrompts();
} catch (error) {
console.error('Failed to delete prompt:', error);
}
}
};
const handleRenamePrompt = async (old_filename, new_filename) => {
try {
await fetch('/rename_prompt', {
method: 'POST',
body: JSON.stringify({ old_filename, new_filename })
});
fetchSavedPrompts();
} catch (error) {
console.error('Failed to rename prompt:', error);
}
};
const startRenamePrompt = (old_filename) => {
setRenameNewName(old_filename);
setRenameOldName(old_filename);
};
function handleKeyDown(old_filename, key) {
if (event.key === 'Enter') {
if (renameOldName !== undefined)
handleRenamePrompt(old_filename, renameNewName);
} else if (event.key === 'Escape') {
if (renameOldName !== undefined)
setRenameOldName(undefined);
}
}
const SortButton = ({ type, icon }) => html`
<button
className="sort-button ${sortBy === type ? 'active' : ''}"
onClick=${() => handleSort(type)}
title="Sort by ${type}"
style=${{'padding': '4px', 'border': 'none', 'background': 'transparent', 'cursor': 'pointer', 'opacity': sortBy === type ? '1' : '0.5'}}>
${icon}
${sortBy === type && html`<${SVG_SortIndicator} sortOrder=${sortOrder} />`}
</button>
`;
const formatFileSize = (bytes) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
};
return html`
<${Modal}
isOpen=${isOpen}
onClose=${closeModal}
title="Saved Prompts"
description="Manage your saved prompts and slots."
style=${{
'height': `${modalHeight}px`,
'resize': 'vertical',
'overflow': 'auto',
'minHeight': '200px',
'maxHeight': '90vh'
}}>
<div
className="saved-prompts-container"
style=${{
'display': 'flex',
'gap': 0,
'height': '100%',
'position': 'relative'
}}>
<!-- Left Panel: List -->
<div
className="prompts-list"
style=${{
'flex': `0 0 ${listWidth}px`,
'overflowY': 'auto',
'borderRight': '1px solid rgba(128,128,128,0.3)',
'paddingRight': '19px'
}}>
<!-- Tab Selector -->
<div
className="hbox"
style=${{
'gap': '4px',
'marginBottom': '8px'
}}>
<button
className="${activeTab === 'saved' ? 'selected' : ''}"
onClick=${() => setActiveTab('saved')}
style=${{ 'flex': 1 }}>
Load Prompts
</button>
<button
className="${activeTab === 'simple' ? 'selected' : ''}"
onClick=${() => setActiveTab('simple')}
style=${{ 'flex': 1 }}>
Save Slots
</button>
</div>
${activeTab === 'saved' ? html`
<!-- Sort Controls -->
<div
className="hbox"
style=${{
'gap': '2px',
'marginBottom': '8px',
'justifyContent': 'space-between'
}}>
<${SortButton} type="name" icon=${html`<${SVG_SortName}/>`}/>
<${SortButton} type="tokens" icon=${html`<${SVG_SortTokens}/>`}/>
<${SortButton} type="size" icon=${html`<${SVG_SortSize}/>`}/>
<${SortButton} type="date" icon=${html`<${SVG_SortDate}/>`}/>
</div>
<!-- Saved Prompts List -->
${loading ? html`
<div style=${{
'textAlign': 'center',
'padding': '20px'
}}>
Loading...
</div>
` : html`
<ul className="Prompts" style=${{
'margin': 0,
'padding': 0
}}>
${sortedPrompts.map(prompt => html`
<li key=${prompt.filename}>
<a className="Session ${selectedPrompt?.filename === prompt.filename ? 'selected' : ''}"
onClick=${() => setSelectedPrompt(prompt)}>
${renameOldName == prompt.filename ? html`
<input
type="text"
value=${renameNewName}
onChange=${(e) => setRenameNewName(e.target.value)}
onKeyDown=${(e) => handleKeyDown(prompt.filename, e.key)}
onClick=${(e) => e.stopPropagation()}
autoFocus
/>
<div className="flex-separator"></div>
<button onClick=${(e) => (handleRenamePrompt(prompt.filename, renameNewName), e.stopPropagation())}><${SVG_Confirm}/></button>
<button onClick=${(e) => (setRenameOldName(undefined), e.stopPropagation())}><${SVG_Cancel}/></button>
` : html`
<div style=${{'display': 'flex', 'flex-direction': 'column', 'flex': 1}}>
<span style=${{'font-weight': 'bold'}}>${prompt.filename.replace('.bin', '')}</span>
<span style=${{'font-size': '0.85em', 'opacity': '0.7'}}>
${prompt.token_count.toLocaleString()} tokens • ${formatFileSize(prompt.filesize)}${new Date(prompt.mtime).toLocaleString()}
</span>
</div>
<div className="flex-separator"></div>
<button
onClick=${(e) => {
e.stopPropagation();
startRenamePrompt(prompt.filename);
}}
title="Load this prompt">
<${SVG_Rename}/>
</button>
<button
onClick=${(e) => {
e.stopPropagation();
handleDeletePrompt(prompt.filename);
}}
title="Delete this prompt">
<${SVG_Trash}/>
</button>`}
</a>
</li>
`)}
</ul>
`}` : html`
<!-- Simple Prompts List -->
<ul className="Prompts" style=${{'margin': 0, 'padding': 0}}>
${simplePrompts.map(prompt => html`
<li key=${prompt.slot_id}>
<a className="Session ${selectedPrompt?.slot_id === prompt.slot_id ? 'selected' : ''}"
onClick=${() => setSelectedPrompt(prompt)}>
<div style=${{'display': 'flex', 'flex-direction': 'column', 'flex': 1}}>
<span style=${{'font-weight': 'bold'}}>Slot #${prompt.slot_id}</span>
<span style=${{'font-size': '0.85em', 'opacity': '0.7'}}>
${prompt.token_count.toLocaleString()} tokens
</span>
</div>
</a>
</li>
`)}
</ul>
`}
</div>
<!-- Resize Handle -->
<div
'style'=${{
'width': '5px',
'cursor': 'col-resize',
'background': 'transparent',
'postion': 'relative'
}}
onMouseDown=${(e) => {
const startX = e.clientX;
const startWidth = listWidth;
const handleMouseMove = (e) => {
const newWidth = Math.max(200, Math.min(600, startWidth + e.clientX - startX));
setListWidth(newWidth);
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}}>
<div style=${{
'position': 'absolute',
'left': '1px',
'top': 0,
'bottom': 0,
'width': '3px',
'background': 'rgba(128,128,128,0.2)'
}}></div>
</div>
<!-- Right Panel: Preview -->
<div className="prompt-preview" style=${{'flex': 1, 'display': 'flex', 'flex-direction': 'column', 'overflow': 'hidden', 'height': '100vh'}}>
${selectedPrompt ? html`
<div style=${{'margin-bottom': '8px', 'padding-bottom': '8px', 'border-bottom': '1px solid rgba(128,128,128,0.3)'}}>
<h3 style=${{'margin': '0 0 4px 0'}}>${Object.hasOwn(selectedPrompt, 'slot_id') ? `Slot #${selectedPrompt.slot_id}` : selectedPrompt.filename.replace('.bin', '')}</h3>
${Object.hasOwn(selectedPrompt, 'slot_id') && html`
<div style=${{'display': 'flex', 'gap': '16px', 'font-size': '0.9em', 'opacity': '0.8'}}>
<span>${selectedPrompt.token_count.toLocaleString()} tokens</span>
<div className="flex-separator"></div>
<${InputBox}
'label'=""
'type'="text"
'placeholder'="Name for saved prompt"
'readOnly'=${!!cancel}
'value'=${saveName}
'onValueChange'=${setSaveName}
style=${{ 'flex': 1, 'width': '10vw'}}/>
<button
onClick=${() => handleSavePrompt(selectedPrompt.slot_id)}
style=${{'font-size': '0.9em'}}>
Save Prompt
</button>
</div>
`}
${Object.hasOwn(selectedPrompt, 'filename') && html`
<div style=${{'display': 'flex', 'gap': '16px', 'font-size': '0.9em', 'opacity': '0.8'}}>
<span>${selectedPrompt.token_count.toLocaleString()} tokens</span>
<span>${formatFileSize(selectedPrompt.filesize)}</span>
<span>${new Date(selectedPrompt.mtime).toLocaleString()}</span>
<div className="flex-separator"></div>
<${SelectBox}
label="Slot"
value=${loadSlot}
onValueChange=${setLoadSlot}
options=${[{ name: '', value: '' }, ...simplePrompts.map(prompt => ({ name: prompt.slot_id, value: prompt.slot_id}))]}/>
<button
onClick=${() => handleLoadPrompt(selectedPrompt.filename)}
style=${{'padding': '2px 8px', 'font-size': '0.9em'}}>
Load This Prompt
</button>
</div>
`}
</div>
<textarea
readOnly
value=${selectedPrompt.prompt || selectedPrompt}
className="expanded-text-area-settings"
style=${{'flex': 1, 'resize': 'none', 'font-family': 'monospace', 'font-size': '0.9em'}}/>
` : html`
<div style=${{'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'height': '100%', 'opacity': '0.5'}}>
Select a saved prompt to preview
</div>
`}
</div>
</div>
</${Modal}>
`;
}
function Widget({ isOpen, onClose, title, id, children, ...props }) {
if (!isOpen) {
return null;
@@ -4952,6 +5430,13 @@ export function App({ sessionStorage, templateStorage, useSessionState, useDBTem
const [promptPreviewReroll, setPromptPreviewReroll] = useState(0);
const [showPromptPreview, setShowPromptPreview] = useSessionState('promptPreview', defaultPresets.promptPreview);
const [promptPreviewTokens, setPromptPreviewTokens] = useSessionState('promptPreviewTokens', defaultPresets.promptPreviewTokens);
const [zstdTable, setZstdTable] = useState('');
const [zstdColumn, setZstdColumn] = useState('');
const [zstdLevel, setZstdLevel] = useState(3);
const [zstdRatio, setZstdRatio] = useState(0.1);
const [showCustomMaintenance, setShowCustomMaintenance] = useState(false);
const [maintenanceDuration, setMaintenanceDuration] = useState('');
const [maintenanceDbLoad, setMaintenanceDbLoad] = useState(0.5);
function replacePlaceholders(string,placeholders) {
// give placeholders as json object
@@ -6865,6 +7350,118 @@ export function App({ sessionStorage, templateStorage, useSessionState, useDBTem
</button>
</${CollapsibleGroup}>
<${CollapsibleGroup} label="Database Tools" expanded>
<div className="hbox">
<button
disabled=${!!cancel}
onClick=${async () => {
if (confirm("Optimize database storage? This may temporarily impact performance.")) {
await fetch('/vacuum', { method: 'GET' });
}
}}>
VACUUM
</button>
<button
disabled=${!!cancel}
onClick=${async () => {
const res = await fetch('/zstd_get_configs');
const data = await res.json();
if (data.ok) {
console.log(data.configs);
}
else {
console.log(data.message);
}
}}>
Show Configs
</button>
</div>
<div className="hbox">
<${InputSlider} label="Compression Level" type="number" step="1" min="1" max="22"
readOnly=${!!cancel} value=${zstdLevel} onValueChange=${setZstdLevel}/>
<${InputSlider} label="Samples Ratio" type="number" step="0.05" max="1"
readOnly=${!!cancel} value=${zstdRatio} onValueChange=${setZstdRatio}/>
<button
disabled=${!!cancel}
onClick=${async () => {
await fetch('/zstd_enable_transparent', {
method: 'POST',
body: JSON.stringify({
table: 'sessions',
column: 'data',
compression_level: zstdLevel,
train_dict_samples_ratio: zstdRatio
})
});
}}>
Enable
</button>
</div>
<div className="horz-separator"></div>
<div className="hbox">
<button
disabled=${!!cancel}
onClick=${async () => {
await fetch('/zstd_incremental_maintenance', {
method: 'POST',
body: JSON.stringify({ duration: null, db_load: 1.0 })
});
}}>
Full Maintenance
</button>
<button
disabled=${!!cancel}
onClick=${async () => {
await fetch('/zstd_incremental_maintenance', {
method: 'POST',
body: JSON.stringify({ duration: 0, db_load: 1.0 })
});
}}>
Single Item
</button>
<button
disabled=${!!cancel}
onClick=${() => setShowCustomMaintenance(!showCustomMaintenance)}>
Custom
</button>
</div>
${showCustomMaintenance && html`
<div className="hbox">
<${InputBox} label="Duration (sec)" type="number"
readOnly=${!!cancel}
value=${maintenanceDuration}
onValueChange=${setMaintenanceDuration}
placeholder="Optional"/>
<${InputSlider} label="DB Load" type="number" step="0.1" max="1"
readOnly=${!!cancel} value=${maintenanceDbLoad} onValueChange=${setMaintenanceDbLoad}/>
<button
disabled=${!!cancel}
onClick=${async () => {
await fetch('/zstd_incremental_maintenance', {
method: 'POST',
body: JSON.stringify({
duration: maintenanceDuration ? parseFloat(maintenanceDuration) : null,
db_load: maintenanceDbLoad
})
});
}}>
Run
</button>
</div>
`}
</${CollapsibleGroup}>
<${CollapsibleGroup} label="Slot and KV Save Mgmt">
<button disabled=${!!cancel} onClick=${() => toggleModal("saved_prompts")}>
Manage Slots and Saved Prompt
</button>
</${CollapsibleGroup}>
${!!tokens && html`
<${InputBox} label="Tokens" value="${tokens}${tokensPerSec ? ` (${tokensPerSec.toFixed(2)} T/s)` : ``}" readOnly/>`}
<div className="buttons">
@@ -7066,6 +7663,11 @@ export function App({ sessionStorage, templateStorage, useSessionState, useDBTem
drySequenceBreakersError=${drySequenceBreakersError}
bannedTokensError=${bannedTokensError}/>
<${SavedPromptsModal}
isOpen=${modalState.saved_prompts}
closeModal=${() => toggleModal("saved_prompts")}
cancel=${cancel}/>
<${EditorContextMenu}
isOpen=${contextMenuState.visible}
closeMenu=${() => setContextMenuState({ visible: false, x: 0, y: 0 })}

View File

@@ -3345,7 +3345,8 @@ int main(int argc, char ** argv) {
{ "system_prompt", ctx_server.system_prompt.c_str() },
{ "default_generation_settings", ctx_server.default_generation_settings_for_props },
{ "total_slots", ctx_server.params.n_parallel },
{ "chat_template", curr_tmpl.c_str() }
{ "chat_template", curr_tmpl.c_str() },
{ "n_ctx", ctx_server.n_ctx }
};
res.set_content(data.dump(), "application/json; charset=utf-8");
@@ -3760,9 +3761,28 @@ int main(int argc, char ** argv) {
std::vector<llama_token> tokens(n_token_count);
file.read(reinterpret_cast<char*>(tokens.data()), tokens.size() * sizeof(llama_token));
//C++17 is not modern enough to have a nice and portable way to get the mtime of a file
//so the following seems to be needed
auto ftime = fs::last_write_time(entry.path());
auto system_time = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()
);
std::time_t c_time = std::chrono::system_clock::to_time_t(system_time);
std::tm tm_struct;
#if defined(_WIN32)
localtime_s(&tm_struct, &c_time);
#else
localtime_r(&c_time, &tm_struct);
#endif
std::ostringstream oss;
oss << std::put_time(&tm_struct, "%Y-%m-%d %H:%M:%S");
auto str_time = oss.str();
response.push_back({
{"filename", entry.path().filename().string()},
{"filesize", entry.file_size()},
{"mtime", str_time},
{"token_count", n_token_count},
{"prompt", tokens_to_str(ctx_server.ctx, tokens.cbegin(), tokens.cend())}
});
@@ -3774,6 +3794,131 @@ int main(int argc, char ** argv) {
res.set_content(response.dump(), "application/json; charset=utf-8");
};
const auto list_slot_prompts = [&ctx_server, &params](const httplib::Request& req, httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
json response = json::array();
for (server_slot & slot : ctx_server.slots) {
response.push_back({
{"slot_id", slot.id},
{"token_count", slot.cache_tokens.size()},
{"prompt", tokens_to_str(ctx_server.ctx, slot.cache_tokens.cbegin(), slot.cache_tokens.cend())}
});
}
res.set_content(response.dump(), "application/json; charset=utf-8");
};
const auto delete_saved_prompt = [&ctx_server, &params](const httplib::Request& req, httplib::Response& res)-> void {
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
json response;
namespace fs = std::filesystem;
try {
const json body = json::parse(req.body);
const std::string filename_str = body.at("filename");
// prevent directory traversal attacks
if (filename_str.find("..") != std::string::npos || filename_str.find('/') != std::string::npos || filename_str.find('\\') != std::string::npos) {
res.status = 400;
response = {{"error", "Invalid filename format."}};
res.set_content(response.dump(), "application/json; charset=utf-8");
return;
}
const fs::path file_to_delete = fs::path(params.slot_save_path) / fs::path(filename_str);
if (!fs::exists(file_to_delete) || !fs::is_regular_file(file_to_delete)) {
res.status = 404;
response = {{"error", "File not found."}};
res.set_content(response.dump(), "application/json; charset=utf-8");
return;
}
if (fs::remove(file_to_delete)) {
response = {
{"status", "deleted"},
{"filename", filename_str}
};
} else {
res.status = 500;
response = {{"error", "Failed to delete the file."}};
}
} catch (const json::parse_error& e) {
res.status = 400;
response = {{"error", "Invalid JSON request body."}};
} catch (const json::out_of_range& e) {
res.status = 400;
response = {{"error", "Missing 'filename' key in request body."}};
} catch (const std::exception& e) {
res.status = 500;
response = {{"error", e.what()}};
}
res.set_content(response.dump(), "application/json; charset=utf-8");
};
const auto rename_saved_prompt = [&ctx_server, &params](const httplib::Request& req, httplib::Response& res)-> void {
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
json response;
namespace fs = std::filesystem;
try {
const json body = json::parse(req.body);
const std::string old_filename_str = body.at("old_filename");
const std::string new_filename_str = body.at("new_filename");
if (old_filename_str.find("..") != std::string::npos || old_filename_str.find_first_of("/\\") != std::string::npos ||
new_filename_str.find("..") != std::string::npos || new_filename_str.find_first_of("/\\") != std::string::npos) {
res.status = 400;
response = {{"error", "Invalid filename format."}};
res.set_content(response.dump(), "application/json; charset=utf-8");
return;
}
const fs::path old_path = fs::path(params.slot_save_path) / old_filename_str;
const fs::path new_path = fs::path(params.slot_save_path) / new_filename_str;
if (!fs::exists(old_path) || !fs::is_regular_file(old_path)) {
res.status = 404;
response = {{"error", "Source file not found."}};
res.set_content(response.dump(), "application/json; charset=utf-8");
return;
}
if (fs::exists(new_path)) {
res.status = 409;
response = {{"error", "Destination filename already exists."}};
res.set_content(response.dump(), "application/json; charset=utf-8");
return;
}
std::error_code ec;
fs::rename(old_path, new_path, ec);
if (ec) {
res.status = 500;
response = {{"error", "Failed to rename file: " + ec.message()}};
} else {
response = {
{"status", "renamed"},
{"old_filename", old_filename_str},
{"new_filename", new_filename_str}
};
}
} catch (const json::parse_error& e) {
res.status = 400;
response = {{"error", "Invalid JSON request body."}};
} catch (const json::out_of_range& e) {
res.status = 400;
response = {{"error", "Missing 'old_filename' or 'new_filename' in request body."}};
} catch (const std::exception& e) {
res.status = 500;
response = {{"error", e.what()}};
}
res.set_content(response.dump(), "application/json; charset=utf-8");
};
auto handle_static_file = [](unsigned char * content, size_t len, const char * mime_type) {
return [content, len, mime_type](const httplib::Request &, httplib::Response & res) {
res.set_content(reinterpret_cast<const char*>(content), len, mime_type);
@@ -3781,7 +3926,6 @@ int main(int argc, char ** argv) {
};
};
const auto handle_version = [&params, sqlite_extension_loaded](const httplib::Request&, httplib::Response& res) {
res.set_content(
json{{"version", 4},
@@ -3967,10 +4111,14 @@ int main(int argc, char ** argv) {
svr->Post("/lora-adapters", handle_lora_adapters_apply);
// Save & load slots
svr->Get ("/slots", handle_slots);
svr->Get ("/slots/list", list_slot_prompts);
if (!params.slot_save_path.empty()) {
// these endpoints rely on slot_save_path existing
svr->Post("/slots/:id_slot", handle_slots_action);
svr->Get ("/list", list_saved_prompts);
svr->Post("/delete_prompt", delete_saved_prompt);
svr->Post("/rename_prompt", rename_saved_prompt);
}
svr->Get ("/version", handle_version);
if (!params.sql_save_file.empty()) {