Archived
feat: use icon actions in chat composer bar
Replace Send/Queue/Steer/Stop text buttons with compact icons, move Attach into the message box, and show the thinking level as a fill gauge. Keeps mobile layouts uncrowded while preserving accessible labels and the readable model selector.
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Declutter the chat composer bar with icon-based actions. The Send, Queue, Steer, and Stop buttons are now compact icons, the Attach button moved into the message box, and the thinking level is shown as a small gauge that fills with the selected level. This leaves more room on narrow/mobile layouts while keeping the model selector readable. All controls retain accessible labels and tooltips.
|
||||||
@@ -14,6 +14,7 @@ import { detectPromptCompletionTrigger, fileCompletionInsertText, type PromptCom
|
|||||||
import { clearDraft, loadDraft, saveDraft } from "../promptDraftStorage";
|
import { clearDraft, loadDraft, saveDraft } from "../promptDraftStorage";
|
||||||
import { loadAttachmentDelivery, saveAttachmentDelivery } from "../attachmentPreferences";
|
import { loadAttachmentDelivery, saveAttachmentDelivery } from "../attachmentPreferences";
|
||||||
import { promptEditorStyles, type CompletionItem } from "./shared";
|
import { promptEditorStyles, type CompletionItem } from "./shared";
|
||||||
|
import { renderAttachIcon, renderSendIcon, renderQueueIcon, renderSteerIcon, renderStopIcon, renderThinkingGauge, thinkingLevelLabel } from "./promptEditorIcons";
|
||||||
import "./AutocompleteMenu";
|
import "./AutocompleteMenu";
|
||||||
|
|
||||||
interface PendingAttachment {
|
interface PendingAttachment {
|
||||||
@@ -90,6 +91,8 @@ export class PromptEditor extends LitElement {
|
|||||||
<footer class=${shellMode ? "shell-mode" : ""} @paste=${(event: ClipboardEvent) => { void this.handlePaste(event); }} @dragover=${(event: DragEvent) => { this.handleDragOver(event); }} @drop=${(event: DragEvent) => { void this.handleDrop(event); }}>
|
<footer class=${shellMode ? "shell-mode" : ""} @paste=${(event: ClipboardEvent) => { void this.handlePaste(event); }} @dragover=${(event: DragEvent) => { this.handleDragOver(event); }} @drop=${(event: DragEvent) => { void this.handleDrop(event); }}>
|
||||||
<div class="editor-wrap">
|
<div class="editor-wrap">
|
||||||
<div class=${`markdown-editor${this.disabled ? " markdown-editor-disabled" : ""}`} aria-label="Message pi" aria-disabled=${this.disabled ? "true" : "false"}></div>
|
<div class=${`markdown-editor${this.disabled ? " markdown-editor-disabled" : ""}`} aria-label="Message pi" aria-disabled=${this.disabled ? "true" : "false"}></div>
|
||||||
|
<input class="attachment-input" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple hidden @change=${(event: Event) => { void this.handleFileInput(event); }} />
|
||||||
|
<button class="editor-attach icon-button" ?disabled=${busy} title="Attach images" aria-label="Attach images" @click=${() => { this.attachmentInput?.click(); }}>${renderAttachIcon()}</button>
|
||||||
${shellMode ? html`<div class="mode-hint">Shell command${inputMode.excludeFromContext ? " · excluded from context" : ""}</div>` : null}
|
${shellMode ? html`<div class="mode-hint">Shell command${inputMode.excludeFromContext ? " · excluded from context" : ""}</div>` : null}
|
||||||
${this.isCompacting && !shellMode ? html`<div class="mode-hint">Compacting history · message will be queued</div>` : null}
|
${this.isCompacting && !shellMode ? html`<div class="mode-hint">Compacting history · message will be queued</div>` : null}
|
||||||
${this.renderAttachments()}
|
${this.renderAttachments()}
|
||||||
@@ -97,11 +100,9 @@ export class PromptEditor extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
${this.renderCompactStatus()}
|
${this.renderCompactStatus()}
|
||||||
<input class="attachment-input" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple hidden @change=${(event: Event) => { void this.handleFileInput(event); }} />
|
<button class="icon-button send-button" ?disabled=${busy} title=${queuesInput ? "Queue until the current activity finishes" : "Send message"} aria-label=${queuesInput ? "Queue message" : "Send message"} @click=${() => { this.send("followUp"); }}>${queuesInput ? renderQueueIcon() : renderSendIcon()}</button>
|
||||||
<button class="attach-button" ?disabled=${busy} title="Attach images" @click=${() => { this.attachmentInput?.click(); }}>Attach</button>
|
${this.canSteer && !this.isCompacting ? html`<button class="icon-button steer-button" ?disabled=${busy} title="Steer the current response before the next model call" aria-label="Steer current response" @click=${() => { this.send("steer"); }}>${renderSteerIcon()}</button>` : null}
|
||||||
<button ?disabled=${busy} title=${queuesInput ? "Queue until the current activity finishes" : "Send message"} @click=${() => { this.send("followUp"); }}>${queuesInput ? "Queue" : "Send"}</button>
|
<button class="icon-button stop-button" ?disabled=${this.disabled || !this.canStop} title=${this.canStop ? "Stop current work and clear queued messages" : "Nothing running"} aria-label="Stop current work" @click=${() => this.onStop?.()}>${renderStopIcon()}</button>
|
||||||
${this.canSteer && !this.isCompacting ? html`<button ?disabled=${busy} title="Steer the current response before the next model call" @click=${() => { this.send("steer"); }}>Steer</button>` : null}
|
|
||||||
<button ?disabled=${this.disabled || !this.canStop} title=${this.canStop ? "Stop current work and clear queued messages" : "Nothing running"} @click=${() => this.onStop?.()}>Stop</button>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
`;
|
`;
|
||||||
@@ -119,7 +120,7 @@ export class PromptEditor extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="compact-status" aria-label="Session status">
|
<div class="compact-status" aria-label="Session status">
|
||||||
<button class="select-model" title="Select model" @click=${() => this.onSelectModel?.()}>${provider}${model}</button>
|
<button class="select-model" title="Select model" @click=${() => this.onSelectModel?.()}>${provider}${model}</button>
|
||||||
<button class="select-thinking" title="Select thinking level" @click=${() => this.onSelectThinking?.()}>think ${status.thinkingLevel ?? "off"}</button>
|
<button class="select-thinking icon-button" title=${`Thinking level: ${thinkingLevelLabel(status.thinkingLevel)}`} aria-label=${`Thinking level: ${thinkingLevelLabel(status.thinkingLevel)}`} @click=${() => this.onSelectThinking?.()}>${renderThinkingGauge(status.thinkingLevel)}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { svg, type TemplateResult } from "lit";
|
||||||
|
|
||||||
|
// Hand-rolled inline icons matching the project's stroke style
|
||||||
|
// (viewBox 0 0 24 24, fill none, stroke currentColor, round caps/joins).
|
||||||
|
// See tabIcons.ts for the established convention.
|
||||||
|
|
||||||
|
export function renderAttachIcon(): TemplateResult {
|
||||||
|
return svg`
|
||||||
|
<svg class="prompt-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M20 11.5 12.5 19a4 4 0 0 1-5.66-5.66l7.07-7.07a2.5 2.5 0 0 1 3.54 3.54l-7.07 7.07a1 1 0 0 1-1.42-1.42l6.37-6.36"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSendIcon(): TemplateResult {
|
||||||
|
return svg`
|
||||||
|
<svg class="prompt-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M21 3 13.5 21l-2-8.5L3 10.5Z"></path>
|
||||||
|
<path d="M21 3 11.5 12.5"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderQueueIcon(): TemplateResult {
|
||||||
|
return svg`
|
||||||
|
<svg class="prompt-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M4 7h11"></path>
|
||||||
|
<path d="M4 12h7"></path>
|
||||||
|
<path d="M4 17h7"></path>
|
||||||
|
<path d="m15 14 5 3-5 3z"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSteerIcon(): TemplateResult {
|
||||||
|
// Steer and send are both "do this now"; the queue icon carries the "later" distinction.
|
||||||
|
return renderSendIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderStopIcon(): TemplateResult {
|
||||||
|
return svg`
|
||||||
|
<svg class="prompt-action-icon prompt-action-icon-filled" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<rect x="6.5" y="6.5" width="11" height="11" rx="2"></rect>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const THINKING_LEVEL_STEPS: Record<string, number> = {
|
||||||
|
off: 0,
|
||||||
|
minimal: 1,
|
||||||
|
low: 2,
|
||||||
|
medium: 3,
|
||||||
|
high: 4,
|
||||||
|
xhigh: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function thinkingLevelLabel(level: string | undefined): string {
|
||||||
|
return level === undefined || level === "" ? "off" : level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A 5-bar gauge that fills up to the active thinking level. */
|
||||||
|
export function renderThinkingGauge(level: string | undefined): TemplateResult {
|
||||||
|
const steps = THINKING_LEVEL_STEPS[level ?? "off"] ?? 0;
|
||||||
|
const bars = [0, 1, 2, 3, 4].map((i) => {
|
||||||
|
const x = 3 + i * 4;
|
||||||
|
const height = 4 + i * 3;
|
||||||
|
const y = 20 - height;
|
||||||
|
const active = i < steps;
|
||||||
|
return svg`<rect class=${active ? "gauge-bar gauge-bar-active" : "gauge-bar"} x=${x} y=${y} width="2.6" height=${height} rx="1"></rect>`;
|
||||||
|
});
|
||||||
|
return svg`
|
||||||
|
<svg class="prompt-thinking-gauge" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
${bars}
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -454,16 +454,24 @@ export const promptEditorStyles = css`
|
|||||||
.compact-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: var(--pi-muted); font-size: 12px; flex: 1 1 0; }
|
.compact-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: var(--pi-muted); font-size: 12px; flex: 1 1 0; }
|
||||||
.compact-status > button { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
.compact-status > button { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.select-model { max-width: min(42vw, 320px); }
|
.select-model { max-width: min(42vw, 320px); }
|
||||||
.select-thinking { max-width: 110px; }
|
.icon-button { flex: 0 0 auto; display: inline-grid; place-items: center; width: 36px; height: 36px; padding: 0; }
|
||||||
|
.icon-button .prompt-action-icon, .icon-button .prompt-thinking-gauge { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
||||||
|
.icon-button .prompt-action-icon-filled { fill: currentColor; stroke: none; }
|
||||||
|
.send-button:not(:disabled) { color: var(--pi-accent, var(--pi-text)); }
|
||||||
|
.stop-button:not(:disabled) { color: var(--pi-danger); }
|
||||||
|
.select-thinking .prompt-thinking-gauge .gauge-bar { fill: currentColor; stroke: none; opacity: .28; }
|
||||||
|
.select-thinking .prompt-thinking-gauge .gauge-bar-active { opacity: 1; }
|
||||||
|
.editor-attach { position: absolute; right: 8px; bottom: 8px; z-index: 2; width: 30px; height: 30px; }
|
||||||
|
.editor-attach .prompt-action-icon { width: 16px; height: 16px; }
|
||||||
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid var(--pi-border); background: var(--pi-bg); color: var(--pi-text); font: 16px/1.4 system-ui, sans-serif; }
|
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid var(--pi-border); background: var(--pi-bg); color: var(--pi-text); font: 16px/1.4 system-ui, sans-serif; }
|
||||||
textarea { overflow-y: auto; padding: 8px; }
|
textarea { overflow-y: auto; padding: 8px; }
|
||||||
.markdown-editor .cm-scroller { max-height: 220px; overflow-y: auto; font-family: system-ui, sans-serif; line-height: 1.4; }
|
.markdown-editor .cm-scroller { max-height: 220px; overflow-y: auto; font-family: system-ui, sans-serif; line-height: 1.4; }
|
||||||
.markdown-editor .cm-content { min-height: 38px; padding: 8px; caret-color: var(--pi-text); }
|
.markdown-editor .cm-content { min-height: 38px; padding: 8px 44px 8px 8px; caret-color: var(--pi-text); }
|
||||||
.markdown-editor .cm-line { padding: 0; }
|
.markdown-editor .cm-line { padding: 0; }
|
||||||
.markdown-editor .cm-placeholder { color: var(--pi-dim); }
|
.markdown-editor .cm-placeholder { color: var(--pi-dim); }
|
||||||
.markdown-editor .cm-focused { outline: none; }
|
.markdown-editor .cm-focused { outline: none; }
|
||||||
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: var(--pi-success); box-shadow: 0 0 0 1px var(--pi-success-ring); }
|
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: var(--pi-success); box-shadow: 0 0 0 1px var(--pi-success-ring); }
|
||||||
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 2px 8px; font-size: 12px; pointer-events: none; }
|
.mode-hint { position: absolute; right: 46px; bottom: 8px; max-width: calc(100% - 54px); border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 2px 8px; font-size: 12px; pointer-events: none; }
|
||||||
.attachments { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
|
.attachments { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
|
||||||
.attachment-chip { position: relative; width: 56px; height: 56px; border: 1px solid var(--pi-border); border-radius: 8px; overflow: hidden; background: var(--pi-bg); }
|
.attachment-chip { position: relative; width: 56px; height: 56px; border: 1px solid var(--pi-border); border-radius: 8px; overflow: hidden; background: var(--pi-bg); }
|
||||||
.attachment-chip img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
.attachment-chip img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
@@ -482,7 +490,7 @@ export const promptEditorStyles = css`
|
|||||||
@media (max-width: 430px) {
|
@media (max-width: 430px) {
|
||||||
.compact-status { flex-basis: 170px; font-size: 11px; }
|
.compact-status { flex-basis: 170px; font-size: 11px; }
|
||||||
.select-model { max-width: 48vw; }
|
.select-model { max-width: 48vw; }
|
||||||
.select-thinking { max-width: 70px; }
|
|
||||||
button { padding: 5px 7px; }
|
button { padding: 5px 7px; }
|
||||||
|
.icon-button { width: 34px; height: 34px; }
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user