pi-interview 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +346 -0
- package/bin/install.js +87 -0
- package/form/index.html +119 -0
- package/form/script.js +2213 -0
- package/form/styles.css +1394 -0
- package/form/themes/default-dark.css +5 -0
- package/form/themes/default-light.css +24 -0
- package/form/themes/tufte-dark.css +29 -0
- package/form/themes/tufte-light.css +29 -0
- package/index.ts +354 -0
- package/package.json +31 -0
- package/schema.ts +236 -0
- package/server.ts +765 -0
- package/settings.ts +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Interview Tool
|
|
2
|
+
|
|
3
|
+
A custom tool for pi-agent that opens a web-based form to gather user responses to clarification questions.
|
|
4
|
+
|
|
5
|
+
https://github.com/user-attachments/assets/52285bd9-956e-4020-aca5-9fbd82916934
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Clone or copy this directory to your pi-agent extensions folder:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Clone to user extensions directory (available in all projects)
|
|
13
|
+
git clone https://github.com/nicobailon/pi-interview-tool ~/.pi/agent/extensions/interview
|
|
14
|
+
|
|
15
|
+
# Or copy manually
|
|
16
|
+
cp -r /path/to/interview ~/.pi/agent/extensions/
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The tool is automatically discovered on next pi session. No build step required.
|
|
20
|
+
|
|
21
|
+
**Requirements:**
|
|
22
|
+
- pi-agent v0.35.0 or later (extensions API)
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Question Types**: Single-select, multi-select, text input, and image upload
|
|
27
|
+
- **"Other" Option**: Single/multi select questions support custom text input
|
|
28
|
+
- **Per-Question Attachments**: Attach images to any question via button, paste, or drag & drop
|
|
29
|
+
- **Keyboard Navigation**: Full keyboard support with arrow keys, Tab, Enter
|
|
30
|
+
- **Auto-save**: Responses saved to localStorage, restored on reload
|
|
31
|
+
- **Session Timeout**: Configurable timeout with countdown badge, refreshes on activity
|
|
32
|
+
- **Multi-Agent Support**: Queue detection prevents focus stealing when multiple agents run interviews
|
|
33
|
+
- **Queue Toast Switcher**: Active interviews show a top-right toast with a dropdown to open queued sessions
|
|
34
|
+
- **Session Recovery**: Abandoned/timed-out interviews save questions for later retry
|
|
35
|
+
- **Session Status Bar**: Shows project path, git branch, and session ID for identification
|
|
36
|
+
- **Image Support**: Drag & drop anywhere on question, file picker, paste image or path
|
|
37
|
+
- **Path Normalization**: Handles shell-escaped paths (`\ `) and macOS screenshot filenames (narrow no-break space before AM/PM)
|
|
38
|
+
- **Themes**: Built-in default + optional light/dark + custom theme CSS
|
|
39
|
+
|
|
40
|
+
## How It Works
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌─────────┐ ┌──────────────────────────────────────────┐ ┌─────────┐
|
|
44
|
+
│ Agent │ │ Browser Form │ │ Agent │
|
|
45
|
+
│ invokes ├─────►│ ├─────►│receives │
|
|
46
|
+
│interview│ │ answer → answer → attach img → answer │ │responses│
|
|
47
|
+
└─────────┘ │ ↑ │ └─────────┘
|
|
48
|
+
│ └── auto-save, timeout resets ───────┤
|
|
49
|
+
└──────────────────────────────────────────┘
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Lifecycle:**
|
|
53
|
+
1. Agent calls `interview()` → local server starts → browser opens form
|
|
54
|
+
2. User answers at their own pace; each change auto-saves and resets the timeout
|
|
55
|
+
3. Session ends via:
|
|
56
|
+
- **Submit** (`⌘+Enter`) → responses returned to agent
|
|
57
|
+
- **Timeout** → warning overlay, option to stay or close
|
|
58
|
+
- **Escape × 2** → quick cancel
|
|
59
|
+
4. Window closes automatically; agent receives responses (or `null` if cancelled)
|
|
60
|
+
|
|
61
|
+
**Timeout behavior:** The countdown (visible in corner) resets on any activity - typing, clicking, or mouse movement. When it expires, an overlay appears giving the user a chance to continue. Progress is never lost thanks to localStorage auto-save.
|
|
62
|
+
|
|
63
|
+
**Multi-agent behavior:** When multiple agents run interviews simultaneously, only the first auto-opens the browser. Subsequent interviews are queued and shown as a URL in the tool output, preventing focus stealing. Active interviews also surface a top-right toast with a dropdown to open queued sessions. A session status bar at the top of each form shows the project path, git branch, and session ID for easy identification.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
The interview tool is invoked by pi-agent, not imported directly:
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
// Create a questions JSON file, then call the tool
|
|
71
|
+
await interview({
|
|
72
|
+
questions: '/path/to/questions.json',
|
|
73
|
+
timeout: 600, // optional, seconds (default: 600)
|
|
74
|
+
verbose: false // optional, debug logging
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Question Schema
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"title": "Project Setup",
|
|
83
|
+
"description": "Optional description text",
|
|
84
|
+
"questions": [
|
|
85
|
+
{
|
|
86
|
+
"id": "framework",
|
|
87
|
+
"type": "single",
|
|
88
|
+
"question": "Which framework?",
|
|
89
|
+
"options": ["React", "Vue", "Svelte"],
|
|
90
|
+
"recommended": "React"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "features",
|
|
94
|
+
"type": "multi",
|
|
95
|
+
"question": "Which features?",
|
|
96
|
+
"context": "Select all that apply",
|
|
97
|
+
"options": ["Auth", "Database", "API"],
|
|
98
|
+
"recommended": ["Auth", "Database"]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "notes",
|
|
102
|
+
"type": "text",
|
|
103
|
+
"question": "Additional requirements?"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "mockup",
|
|
107
|
+
"type": "image",
|
|
108
|
+
"question": "Upload a design mockup"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Question Fields
|
|
115
|
+
|
|
116
|
+
| Field | Type | Description |
|
|
117
|
+
|-------|------|-------------|
|
|
118
|
+
| `id` | string | Unique identifier |
|
|
119
|
+
| `type` | string | `single`, `multi`, `text`, or `image` |
|
|
120
|
+
| `question` | string | Question text |
|
|
121
|
+
| `options` | string[] or object[] | Choices (required for single/multi). Can be strings or `{ label, code? }` objects |
|
|
122
|
+
| `recommended` | string or string[] | Highlighted option(s) with `*` indicator |
|
|
123
|
+
| `context` | string | Help text shown below question |
|
|
124
|
+
| `codeBlock` | object | Code block displayed below question text |
|
|
125
|
+
|
|
126
|
+
### Code Blocks
|
|
127
|
+
|
|
128
|
+
Questions and options can include code blocks for displaying code snippets, diffs, and file references.
|
|
129
|
+
|
|
130
|
+
**Question-level code block** (displayed above options):
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"id": "review",
|
|
134
|
+
"type": "single",
|
|
135
|
+
"question": "Review this implementation",
|
|
136
|
+
"codeBlock": {
|
|
137
|
+
"code": "function add(a, b) {\n return a + b;\n}",
|
|
138
|
+
"lang": "ts",
|
|
139
|
+
"file": "src/math.ts",
|
|
140
|
+
"lines": "10-12",
|
|
141
|
+
"highlights": [2]
|
|
142
|
+
},
|
|
143
|
+
"options": ["Approve", "Request changes"]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Options with code blocks**:
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"options": [
|
|
151
|
+
{
|
|
152
|
+
"label": "Use async/await",
|
|
153
|
+
"code": { "code": "const data = await fetch(url);", "lang": "ts" }
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"label": "Use promises",
|
|
157
|
+
"code": { "code": "fetch(url).then(data => ...);", "lang": "ts" }
|
|
158
|
+
},
|
|
159
|
+
"Keep current implementation"
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Diff display** (set `lang: "diff"`):
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"codeBlock": {
|
|
168
|
+
"code": "--- a/file.ts\n+++ b/file.ts\n@@ -1,3 +1,4 @@\n const x = 1;\n+const y = 2;\n const z = 3;",
|
|
169
|
+
"lang": "diff",
|
|
170
|
+
"file": "src/file.ts"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| CodeBlock Field | Type | Description |
|
|
176
|
+
|-----------------|------|-------------|
|
|
177
|
+
| `code` | string | The code content (required) |
|
|
178
|
+
| `lang` | string | Language for display (e.g., "ts", "diff") |
|
|
179
|
+
| `file` | string | File path to display in header |
|
|
180
|
+
| `lines` | string | Line range to display (e.g., "10-25") |
|
|
181
|
+
| `highlights` | number[] | Line numbers to highlight |
|
|
182
|
+
| `title` | string | Optional title above code |
|
|
183
|
+
|
|
184
|
+
Line numbers are shown when `file` or `lines` is specified. Diff syntax (`+`/`-` lines) is automatically styled when `lang` is "diff".
|
|
185
|
+
|
|
186
|
+
## Keyboard Shortcuts
|
|
187
|
+
|
|
188
|
+
| Key | Action |
|
|
189
|
+
|-----|--------|
|
|
190
|
+
| `↑` `↓` | Navigate options |
|
|
191
|
+
| `←` `→` | Navigate between questions |
|
|
192
|
+
| `Tab` | Cycle through options |
|
|
193
|
+
| `Enter` / `Space` | Select option |
|
|
194
|
+
| `⌘+V` | Paste image or file path |
|
|
195
|
+
| `⌘+Enter` | Submit form |
|
|
196
|
+
| `Esc` | Show exit overlay (press twice to quit) |
|
|
197
|
+
| `⌘+Shift+L` | Toggle theme (if enabled; appears in shortcuts bar) |
|
|
198
|
+
|
|
199
|
+
## Configuration
|
|
200
|
+
|
|
201
|
+
Settings in `~/.pi/agent/settings.json`:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"interview": {
|
|
206
|
+
"timeout": 600,
|
|
207
|
+
"port": 19847,
|
|
208
|
+
"theme": {
|
|
209
|
+
"mode": "auto",
|
|
210
|
+
"name": "default",
|
|
211
|
+
"lightPath": "/path/to/light.css",
|
|
212
|
+
"darkPath": "/path/to/dark.css",
|
|
213
|
+
"toggleHotkey": "mod+shift+l"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Timeout precedence**: params > settings > default (600s)
|
|
220
|
+
|
|
221
|
+
**Port setting**: Set a fixed `port` (e.g., `19847`) to use a consistent port across sessions.
|
|
222
|
+
|
|
223
|
+
**Theme notes:**
|
|
224
|
+
- `mode`: `dark` (default), `light`, or `auto` (follows OS unless overridden)
|
|
225
|
+
- `name`: built-in themes are `default` and `tufte`
|
|
226
|
+
- `lightPath` / `darkPath`: optional CSS file paths (absolute or relative to cwd)
|
|
227
|
+
- `toggleHotkey`: optional; when set, toggles light/dark and persists per browser profile
|
|
228
|
+
|
|
229
|
+
## Theming
|
|
230
|
+
|
|
231
|
+
The interview form supports light/dark themes with automatic OS detection and user override.
|
|
232
|
+
|
|
233
|
+
### Built-in Themes
|
|
234
|
+
|
|
235
|
+
| Theme | Description |
|
|
236
|
+
|-------|-------------|
|
|
237
|
+
| `default` | Monospace, IDE-inspired aesthetic |
|
|
238
|
+
| `tufte` | Serif fonts (Cormorant Garamond), book-like feel |
|
|
239
|
+
|
|
240
|
+
### Theme Modes
|
|
241
|
+
|
|
242
|
+
- **`dark`** (default): Dark background, light text
|
|
243
|
+
- **`light`**: Light background, dark text
|
|
244
|
+
- **`auto`**: Follows OS preference, user can toggle and override persists in localStorage
|
|
245
|
+
|
|
246
|
+
### Custom Themes
|
|
247
|
+
|
|
248
|
+
Create custom CSS files that override the default variables:
|
|
249
|
+
|
|
250
|
+
```css
|
|
251
|
+
:root {
|
|
252
|
+
--bg-body: #f8f8f8;
|
|
253
|
+
--bg-card: #ffffff;
|
|
254
|
+
--bg-elevated: #f0f0f0;
|
|
255
|
+
--bg-selected: #d0d0e0;
|
|
256
|
+
--bg-hover: #e8e8e8;
|
|
257
|
+
--fg: #1a1a1a;
|
|
258
|
+
--fg-muted: #6c6c6c;
|
|
259
|
+
--fg-dim: #8a8a8a;
|
|
260
|
+
--accent: #5f8787;
|
|
261
|
+
--accent-hover: #4a7272;
|
|
262
|
+
--accent-muted: rgba(95, 135, 135, 0.15);
|
|
263
|
+
--border: #5f87af;
|
|
264
|
+
--border-muted: #b0b0b0;
|
|
265
|
+
--border-focus: #8a8a9a;
|
|
266
|
+
--border-active: #9090a0;
|
|
267
|
+
--success: #87af87;
|
|
268
|
+
--warning: #d7af5f;
|
|
269
|
+
--error: #af5f5f;
|
|
270
|
+
--focus-ring: rgba(95, 135, 175, 0.2);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Then reference in settings or params:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"interview": {
|
|
279
|
+
"theme": {
|
|
280
|
+
"mode": "auto",
|
|
281
|
+
"lightPath": "~/my-themes/light.css",
|
|
282
|
+
"darkPath": "~/my-themes/dark.css",
|
|
283
|
+
"toggleHotkey": "mod+shift+l"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Toggle Hotkey
|
|
290
|
+
|
|
291
|
+
When `toggleHotkey` is set (e.g., `"mod+shift+l"`), users can switch between light/dark modes. The preference persists in the browser's localStorage across sessions.
|
|
292
|
+
|
|
293
|
+
## Response Format
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
interface Response {
|
|
297
|
+
id: string;
|
|
298
|
+
value: string | string[];
|
|
299
|
+
attachments?: string[]; // image paths attached to non-image questions
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
```
|
|
305
|
+
- framework: React [attachments: /path/to/diagram.png]
|
|
306
|
+
- features: Auth, Database
|
|
307
|
+
- notes: Need SSO support
|
|
308
|
+
- mockup: /tmp/uploaded-image.png
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## File Structure
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
interview/
|
|
315
|
+
├── index.ts # Tool entry point, parameter schema
|
|
316
|
+
├── settings.ts # Shared settings module
|
|
317
|
+
├── server.ts # HTTP server, request handling
|
|
318
|
+
├── schema.ts # TypeScript interfaces for questions/responses
|
|
319
|
+
└── form/
|
|
320
|
+
├── index.html # Form template
|
|
321
|
+
├── styles.css # Base styles (dark tokens)
|
|
322
|
+
├── themes/ # Theme overrides (light/dark)
|
|
323
|
+
└── script.js # Form logic, keyboard nav, image handling
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Session Recovery
|
|
327
|
+
|
|
328
|
+
If an interview times out or is abandoned (tab closed, lost connection), the questions are automatically saved to `~/.pi/interview-recovery/` for later retry.
|
|
329
|
+
|
|
330
|
+
**Recovery files:**
|
|
331
|
+
- Location: `~/.pi/interview-recovery/`
|
|
332
|
+
- Format: `{date}_{time}_{project}_{branch}_{sessionId}.json`
|
|
333
|
+
- Example: `2026-01-02_093000_myproject_main_65bec3f4.json`
|
|
334
|
+
- Auto-cleanup: Files older than 7 days are deleted
|
|
335
|
+
|
|
336
|
+
**To retry an abandoned interview:**
|
|
337
|
+
```javascript
|
|
338
|
+
interview({ questions: "~/.pi/interview-recovery/2026-01-02_093000_myproject_main_65bec3f4.json" })
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Limits
|
|
342
|
+
|
|
343
|
+
- Max 12 images total per submission
|
|
344
|
+
- Max 5MB per image
|
|
345
|
+
- Max 4096x4096 pixels per image
|
|
346
|
+
- Allowed types: PNG, JPG, GIF, WebP
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
const EXTENSION_NAME = "interview";
|
|
8
|
+
const TARGET_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", EXTENSION_NAME);
|
|
9
|
+
const SOURCE_DIR = path.join(__dirname, "..");
|
|
10
|
+
|
|
11
|
+
const FILES_TO_COPY = [
|
|
12
|
+
"index.ts",
|
|
13
|
+
"schema.ts",
|
|
14
|
+
"server.ts",
|
|
15
|
+
"settings.ts",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const DIRS_TO_COPY = ["form"];
|
|
19
|
+
|
|
20
|
+
function ensureDir(dir) {
|
|
21
|
+
if (!fs.existsSync(dir)) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function copyFile(src, dest) {
|
|
27
|
+
fs.copyFileSync(src, dest);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function copyDir(src, dest) {
|
|
31
|
+
ensureDir(dest);
|
|
32
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const srcPath = path.join(src, entry.name);
|
|
35
|
+
const destPath = path.join(dest, entry.name);
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
copyDir(srcPath, destPath);
|
|
38
|
+
} else {
|
|
39
|
+
copyFile(srcPath, destPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getVersion() {
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, "package.json"), "utf-8"));
|
|
47
|
+
return pkg.version;
|
|
48
|
+
} catch {
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function main() {
|
|
54
|
+
const version = getVersion();
|
|
55
|
+
console.log(`\npi-interview v${version}`);
|
|
56
|
+
console.log("Installing to:", TARGET_DIR);
|
|
57
|
+
console.log("");
|
|
58
|
+
|
|
59
|
+
ensureDir(TARGET_DIR);
|
|
60
|
+
|
|
61
|
+
// Copy individual files
|
|
62
|
+
for (const file of FILES_TO_COPY) {
|
|
63
|
+
const src = path.join(SOURCE_DIR, file);
|
|
64
|
+
const dest = path.join(TARGET_DIR, file);
|
|
65
|
+
if (fs.existsSync(src)) {
|
|
66
|
+
copyFile(src, dest);
|
|
67
|
+
console.log(" Copied:", file);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Copy directories
|
|
72
|
+
for (const dir of DIRS_TO_COPY) {
|
|
73
|
+
const src = path.join(SOURCE_DIR, dir);
|
|
74
|
+
const dest = path.join(TARGET_DIR, dir);
|
|
75
|
+
if (fs.existsSync(src)) {
|
|
76
|
+
copyDir(src, dest);
|
|
77
|
+
console.log(" Copied:", dir + "/");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log("Installation complete!");
|
|
83
|
+
console.log("Restart pi to load the extension.");
|
|
84
|
+
console.log("");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main();
|
package/form/index.html
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Interview</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css?session=__SESSION_TOKEN__">
|
|
8
|
+
<link rel="stylesheet" href="/theme-light.css?session=__SESSION_TOKEN__" data-theme-link="light" media="(prefers-color-scheme: light)">
|
|
9
|
+
<link rel="stylesheet" href="/theme-dark.css?session=__SESSION_TOKEN__" data-theme-link="dark" media="(prefers-color-scheme: dark)">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<main class="interview-container">
|
|
13
|
+
<div class="session-bar">
|
|
14
|
+
<span class="session-project" id="session-project"></span>
|
|
15
|
+
<span class="session-id" id="session-id"></span>
|
|
16
|
+
</div>
|
|
17
|
+
<header class="interview-header">
|
|
18
|
+
<div class="header-row">
|
|
19
|
+
<h1 id="form-title"></h1>
|
|
20
|
+
</div>
|
|
21
|
+
<p id="form-description"></p>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<form id="interview-form" novalidate>
|
|
25
|
+
<div id="questions-container" role="list"></div>
|
|
26
|
+
|
|
27
|
+
<div id="error-container" aria-live="polite" class="error-message hidden"></div>
|
|
28
|
+
|
|
29
|
+
<footer class="form-footer">
|
|
30
|
+
<button type="submit" id="submit-btn" class="btn-primary">Submit</button>
|
|
31
|
+
</footer>
|
|
32
|
+
</form>
|
|
33
|
+
|
|
34
|
+
<nav class="shortcuts-bar" aria-label="Keyboard shortcuts">
|
|
35
|
+
<div class="shortcut">
|
|
36
|
+
<span class="shortcut-keys"><kbd>↑</kbd><kbd>↓</kbd> <kbd>Tab</kbd></span>
|
|
37
|
+
<span class="shortcut-label">Options</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="shortcut">
|
|
40
|
+
<span class="shortcut-keys"><kbd>←</kbd><kbd>→</kbd></span>
|
|
41
|
+
<span class="shortcut-label">Questions</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="shortcut-divider"></div>
|
|
44
|
+
<div class="shortcut">
|
|
45
|
+
<span class="shortcut-keys"><kbd>Enter</kbd> <kbd>Space</kbd></span>
|
|
46
|
+
<span class="shortcut-label">Select</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="shortcut">
|
|
49
|
+
<span class="shortcut-keys"><kbd class="mod-key">⌘</kbd><kbd>Enter</kbd></span>
|
|
50
|
+
<span class="shortcut-label">Submit</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="shortcut-divider"></div>
|
|
53
|
+
<div class="shortcut">
|
|
54
|
+
<span class="shortcut-keys"><kbd>Esc</kbd></span>
|
|
55
|
+
<span class="shortcut-label">Cancel</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="shortcut-divider"></div>
|
|
58
|
+
<div class="shortcut recommended-hint">
|
|
59
|
+
<span class="shortcut-keys"><span class="star">*</span></span>
|
|
60
|
+
<span class="shortcut-label">Recommended</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="shortcut hidden" data-theme-shortcut>
|
|
63
|
+
<span class="shortcut-keys" data-theme-keys></span>
|
|
64
|
+
<span class="shortcut-label">Theme</span>
|
|
65
|
+
</div>
|
|
66
|
+
</nav>
|
|
67
|
+
|
|
68
|
+
<div id="success-overlay" class="success-overlay hidden" aria-live="polite">
|
|
69
|
+
<div class="success-icon">OK</div>
|
|
70
|
+
<p>Responses submitted</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div id="countdown-badge" class="countdown-badge hidden" aria-live="polite">
|
|
74
|
+
<svg class="countdown-ring" viewBox="0 0 36 36">
|
|
75
|
+
<circle class="countdown-ring-bg" cx="18" cy="18" r="16" />
|
|
76
|
+
<circle class="countdown-ring-progress" cx="18" cy="18" r="16" />
|
|
77
|
+
</svg>
|
|
78
|
+
<span class="countdown-value"></span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div id="queue-toast" class="queue-toast hidden" role="status" aria-live="polite">
|
|
82
|
+
<div class="queue-toast-header">
|
|
83
|
+
<span>Another interview started</span>
|
|
84
|
+
<button type="button" class="queue-toast-close" aria-label="Dismiss">×</button>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="queue-toast-body">
|
|
87
|
+
<label class="queue-toast-label" for="queue-session-select">Switch to:</label>
|
|
88
|
+
<div class="queue-toast-controls">
|
|
89
|
+
<select id="queue-session-select" class="queue-toast-select"></select>
|
|
90
|
+
<button type="button" id="queue-open-btn" class="btn-secondary">Open</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div id="expired-overlay" class="expired-overlay hidden" aria-live="polite">
|
|
96
|
+
<div class="expired-content">
|
|
97
|
+
<div class="expired-icon">!</div>
|
|
98
|
+
<h2>Session Ended</h2>
|
|
99
|
+
<p>The interview session has timed out. Your responses are saved locally and will be restored if restarted.</p>
|
|
100
|
+
<div class="expired-countdown">Closing in <span id="close-countdown">10</span>s</div>
|
|
101
|
+
<div class="expired-actions">
|
|
102
|
+
<button type="button" id="stay-btn" class="btn-primary">Stay Here</button>
|
|
103
|
+
<button type="button" id="close-tab-btn" class="btn-secondary">Close Now</button>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="expired-shortcuts">
|
|
106
|
+
<span><kbd>Enter</kbd> Confirm</span>
|
|
107
|
+
<span><kbd>Tab</kbd> Switch</span>
|
|
108
|
+
<span><kbd>Esc</kbd> Close</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</main>
|
|
113
|
+
|
|
114
|
+
<script>
|
|
115
|
+
window.__INTERVIEW_DATA__ = /* __INTERVIEW_DATA_PLACEHOLDER__ */;
|
|
116
|
+
</script>
|
|
117
|
+
<script src="/script.js?session=__SESSION_TOKEN__"></script>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|