Конфигурируемая платформа для персональных опросов на GitHub Pages с Google Sheets в роли базы данных. Содержание опроса хранится в таблице: сотрудники и токены, активные вопросы, варианты ответов, правильные ответы, принципы, промпт и результаты. Frontend показывает актуальный набор, сохраняет прогресс и формирует персональный протокол. Текущая конфигурация посвящена использованию ИИ, но код не привязан к этой теме или к фиксированному числу вопросов.
Браузер (GitHub Pages) ──JSONP──► Apps Script Web App ──► Google Sheet (единый источник)
| Данные | Лист | Как используется |
|---|---|---|
| Сотрудники и коды | Employees | вход по Токен, статусы Не использован / Частично / Использован, итоги, персональный флажок Таймер |
| Итоги прошлых волн | Employees_history | архив: Волна, ID, ФИО, отдел, должность, все итоговые колонки, дата архивации |
| Тексты вопросов | Questions | ID вопроса / Вопрос / чекбокс Включен / Ограничение (по ID пользователя) |
| Варианты, правильный ответ, теги | Answers | Вариант A–D, Номер правильного ответа, Тег A–D |
| Принципы (7) | Principles | список на приветствии |
| Настройки | Settings | prompt, wave, min_answers (см. ниже) |
| Ответы (лог) | Results | пишется после каждого ответа, колонка Волна; там же заметки проверяющему |
| Признаки вовлечённости | Usages | связка по ID: агент, репозиторий, артефакт, мероприятия, использование инструмента |
Ключи листа Settings:
| Ключ | Значение | Зачем |
|---|---|---|
prompt |
текст | промпт, который сотрудник копирует вместе с протоколом |
wave |
число | номер текущей волны; пишется в колонку Волна листа Results |
min_answers |
{"adoption":6,"interest":6,"safety":4} |
минимум ответов на характеристику: единый источник для frontend, backend и аналитики |
Меняете вопрос/вариант/принцип/промпт в таблице → опрос обновляется при следующем успешном входе.
js/questions.js — локальная генерируемая копия для разработки и не подключается и не публикуется
на GitHub Pages. Публичные тесты используют минимальный контракт вариантов из
tests/fixtures/profile-options.json. Тексты в js/config.js остаются офлайн-фолбэком для
принципов и промпта.
getSurvey и отдельный getQuestions возвращают вопросы только после проверки пары
ID + токен; использованный токен доступа к содержимому опроса не получает.
- Сотрудник вводит персональный токен; backend проверяет его по листу
Employees. - Backend читает сверху вниз только отмеченные строки
Questionsи связывает каждую сAnswersпоID вопроса. - Тип и механика вопроса определяются данными, а не жёстким списком в коде:
| Данные вопроса | Тип | Поведение |
|---|---|---|
Заполнен Номер правильного ответа |
оцениваемый блиц-вопрос | учитывается в проценте правильных; при включённом сотруднику таймере даётся 20 секунд |
В тегах вариантов есть level-* |
самооценка | выбранный тег преобразуется в балл через CONFIG.selfScore |
Нет правильного ответа и level-* |
обычный вопрос | сохраняется выбранный или собственный ответ без таймера |
- После первого сохранённого ответа токен получает статус
Частично. Переходы по вопросам при этом не блокируются сетевыми запросами. - На финале frontend передаёт backend весь текущий комплект ответов. Backend проверяет полноту,
дописывает отсутствующие строки в
Results, сохраняет итоги и последним шагом ставитИспользован. - Протокол строится из того же активного набора: каждый новый включённый вопрос и текст выбранного для него варианта автоматически попадают в скачиваемый файл.
Чекбокс Включен позволяет подготовить следующий набор в тех же листах и переключить опрос без
удаления старых вопросов, вариантов или результатов.
511 самооценка (первый) → 101–110 отношение → 201–210 интерес →
301–310 навыки (таймер 20 сек, оцениваются) → 411 безопасность (последний).
Сейчас включены 32 вопроса; runtime не привязан к этому числу и берёт включённые строки сверху вниз.
Перед первым вопросом блока навыков показывается предупреждение; отсчёт начинается только после
нажатия «Понятно».
В Employees флажок Таймер управляет ограничением индивидуально: установлен — блиц идёт с
20-секундным отсчётом, снят — предупреждение и таймер не показываются. Если колонки нет, backend
сохраняет прежнее поведение и включает таймер.
Правильные ответы блока «Навыки» живут только в таблице (Answers ▸ Номер правильного ответа)
и намеренно не публикуются здесь: ключ, лежащий в открытом репозитории, обесценивает блок.
Backend отдаёт его frontend только вместе с вопросами и только авторизованному сотруднику,
а в скачиваемый протокол правильные ответы не попадают.
Колонка Ограничение (по ID пользователя) на листе Questions решает, кому вопрос
показывается. Пусто — всем. Так можно давать разным респондентам разные наборы, одним
укорачивать опрос, другим удлинять.
| В ячейке | Кому показывается |
|---|---|
| (пусто) | всем |
2,5,7 |
только этим ID |
2-8 |
диапазон ID |
!5 или -5 |
всем, кроме 5 |
кроме 5, 7 |
всем, кроме 5 и 7 (кроме действует до конца строки) |
2-8, !5 |
2, 3, 4, 6, 7, 8 |
все / никому |
явно всем / никому |
Набор считается один раз на запрос и применяется везде: выдача вопросов, приём ответов
(чужой вопрос в Results не попадёт) и проверка полноты при завершении — иначе человек
с укороченным набором не смог бы закончить опрос. Опечатка не угадывается: getQuestions
падает с явным сообщением.
Осторожно с минимумом. Если у человека в наборе осталось меньше вопросов, чем задано
в min_answers, характеристика не считается и портрет выходит пустым. Питают характеристики:
принятие — 8 вопросов (минимум 6), интерес — 8 (6), безопасность — 5 (4); 108 и 110
работают сразу на две. Без последствий для баллов режутся 204, 206, любые 301–310 и 511.
Проверить заранее — auditQuestionSets().
Номер текущей волны — Settings ▸ wave, он пишется в колонку Волна листа Results.
Вся работа с ответами идёт только внутри текущей волны: строки прошлых волн не читаются,
не обновляются и не мешают завершению. Без колонки Волна поведение прежнее — одна общая
куча ответов, и validateSurvey() об этом предупреждает.
Разовая настройка: setupWaveColumn() — добавит колонку и проставит существующим строкам
номер текущей волны.
Перезапуск волны:
- Снять статистику по текущей волне (см. раздел про скилл ниже).
previewNewWave()— показывает, что изменится, ничего не трогая.startNewWave()— архивирует итоги вEmployees_history, очищает итоговые колонки вEmployees, ставитНе использован, увеличиваетSettings ▸ wave. ЛистResultsне трогается вообще.- Снять
Включенсо старых вопросов, поставить новым; вAnswersдобавить варианты с теми же ID (старые строки удалять не нужно). validateSurvey()и рассылка.
ID вопроса нельзя переиспользовать: именно по нему Answers и Results связываются с вопросом.
Порядок строк Questions задаёт порядок показа. Для оцениваемого блиц-вопроса заполняется
Номер правильного ответа, для самооценки — теги level-*. Портретные веса новых вопросов
добавляются в CONFIG.portrait.scores; процент правильных работает автоматически.
Переключайте набор вопросов, когда нет сотрудников со статусом Частично: уже открытая
страница содержит прежний набор.
Запускаются вручную из редактора (Run ▸ имя функции), результат — в Execution log.
| Функция | Что делает |
|---|---|
validateSurvey() |
проверка перед волной: дубли ID и токенов, включённый вопрос без строки в Answers, неверный Номер правильного ответа, битые ограничения, сотрудник без токена, строки Usages с чужим ID, нечитаемый min_answers |
auditQuestionSets() |
кто сколько вопросов получит и у кого набор короче минимума |
setupWaveColumn() |
разово: колонка Волна в Results + заполнение существующих строк |
previewNewWave() |
что изменит startNewWave, без изменений |
startNewWave() |
архивация итогов, сброс статусов, wave + 1 |
Пишутся в строку сотрудника (Employees): Процент правильных, Балл самооценки
(511: level0=0…level3=100), три характеристики 0–100, итоговый Средний балл
и Портрет:
- принятие/готовность использовать ИИ — 8 направленных вопросов блока отношения;
- интерес и инициативность — 8 направленных вопросов блока интереса;
- безопасность и ответственность — 5 вопросов, включая финальный кейс 411;
- итоговый балл = 50% принятие + 30% интерес + 20% безопасность;
- Оппонент ≤40 · Конформист 41–69 · Энтузиаст ≥70.
Веса конкретных вариантов задаются в CONFIG.portrait; собственный ответ без веса
не превращается в нулевой балл. Настройки самооценки — в CONFIG.selfScore.
Минимальное число оценённых ответов на характеристику приходит из таблицы
(Settings ▸ min_answers) и передаётся frontend в ответе getSurvey. Значения в
CONFIG.portrait.dimensions.*.minAnswers остаются запасными: если строки в таблице нет
или backend старый, поведение прежнее.
«Не успел» ≠ «не знал». Когда истекает таймер блиц-вопроса, frontend пишет в Results
маркер TIMEOUT, а не пустую ячейку. Для процента правильных это по-прежнему неверный
ответ, но в аналитике видно причину: нехватка времени и незнание требуют разных выводов.
Кнопка «Скачать протокол» отдаёт .txt с только ФИО + вопрос → ответ. Без ID, без итогов,
без названий блоков, без правильных ответов (чтобы не клеймить и чтобы ключ не расходился).
Протокол формируется из текущих включённых строк, поэтому добавленные в таблицу вопросы и варианты
не требуют отдельных изменений frontend-кода.
Сотрудник берёт этот файл + «Скопировать промпт» и сам получает разбор у ИИ. Оценки и портрет —
внутренние.
- Сам ввод токена не меняет его статус.
- После первого успешно записанного ответа ставится
Частично. - Переход между вопросами не ждёт Apps Script: ответы записываются последовательной фоновой
очередью. Финал не ждёт всю эту очередь: один идемпотентный
finishсинхронизирует все локальные ответы, записывает итоги и только последним действием выставляет статусИспользован. - Повтор
finishпосле сетевого таймаута не создаёт дубли. Если сервер успел завершить опрос, но JSONP-callback потерялся, frontend подтверждает статус отдельным чтением и всё равно открывает финальное видео и скачивание протокола. - Незавершённый опрос можно продолжить с сохранённого в этом браузере места; начать заново нельзя.
- Если после обновления страницы локально уже сохранены все активные ответы, «Продолжить» сразу повторяет
атомарную финализацию и не просит ещё раз отвечать на
411. - Сохранённый прогресс привязан к упорядоченному набору активных ID и не переносится на новый опрос.
- При возврате на уже отвеченный вопрос блока «Навыки» ответ нельзя изменить: таймер повторно не запускается, а кнопка «Вернуться к текущему вопросу» возвращает к сохранённой позиции.
Использованставится только после записи всех активных ответов и итогов. После этого токен блокируется.
- Приветствие:
assets/media/start.mp4; благодарность:assets/media/end.mp4. - Для первых 32 вопросов используются картинки
assets/mascot/1.png … 32.pngбез повторов; в более длинном опросе набор циклически повторяется. Сторона, сдвиг и наклон меняются детерминированно. - Первые маскоты загружаются во время приветствия, затем браузер держит запас следующих четырёх; предыдущая картинка скрывается до готовности новой и не повторяется на соседнем вопросе.
- Широкие изображения автоматически получают отдельное ограничение размера и не заходят на карточку.
- Видео используют
preload="auto"и оптимизированы для веб-показа (960×540, H.264, faststart). - Маркер у принципов:
assets/mascot/hand.png.
- Теги в
Answers: проверить, что портретные теги заполнены в колонкахG:J. - Лист
Settings: создать и заполнить три строки —prompt(готовый текст есть вCONFIG.aiPromptTemplate),wave=1,min_answers={"adoption":6,"interest":6,"safety":4}. - Деплой: Extensions ▸ Apps Script → вставить
backend/Code.gs→ Deploy ▸ Web app (Execute as: Me, Who has access: Anyone) → URL…/exec. - В
js/config.js:DEMO_MODE:false,API_URL:'…/exec'. - Один раз запустить
setupWaveColumn(), затемvalidateSurvey()— и можно рассылать.
В папке skill/ лежит обезличенная копия скилла для Claude Code, которым разбираются
результаты волны: skill/ai-survey-pulse/ (исходники) и skill/ai-survey-pulse.zip
(положить в ~/.claude/skills/). Перед запуском вписать ID своей таблицы
в tools/config.json.
| Инструмент | Что делает |
|---|---|
analyze.py |
качает таблицу, считает агрегаты и разбор по каждому: три характеристики, знания (в процентах от личного набора), калибровка «самооценка − факт», таймауты, вовлечённость с листа Usages, сравнение с прошлой волной |
render_portraits.py |
из portraits.json (текст пишет модель) верстает по одному A4-PDF на человека; падает, если портрет не влез на лист |
make_dashboard.py |
собирает HTML-дашборд: цифры из analysis.json, тексты из story.json |
metrics_column.py |
считает столбец для отдельной таблицы метрик, сверяет с записанным, показывает просроченные мероприятия и сработавшие антиметрики |
python tools/analyze.py --out DIR --fetch # текущая волна
python tools/analyze.py --out DIR --fetch --list-waves # какие волны есть в таблице
python tools/analyze.py --out DIR --fetch --wave 1 # прошлая волна из Employees_history
Скилл только читает таблицы (по export-ссылке, без авторизации) — писать в них он не умеет. Портреты и снимки волн содержат ФИО и баллы: держите рабочую папку вне репозитория и не коммитьте.
При изменении backend/Code.gs обновите существующий deployment новой версией, чтобы сохранить
тот же URL /exec.
Production-режим включён: CONFIG.DEMO_MODE=false, frontend обращается к опубликованному
Google Apps Script из CONFIG.API_URL. js/questions.js остаётся локальной генерируемой копией;
тексты в js/config.js — офлайн-фолбэк принципов и промпта.
GitHub Pages: Settings ▸ Pages ▸ Deploy from a branch ▸ выбрать публикуемую ветку / root.
Версия в query-параметрах CSS/JS в index.html обновляется при релизах, меняющих frontend,
чтобы браузеры не запускали старый код из HTTP-кеша.
node tests/run-tests.jsПроверяются динамическая загрузка и отключение вопросов, произвольные ID, попадание новых вопросов и
ответов в протокол, статусы токена, атомарное завершение, мгновенный переход при фоновом сохранении,
восстановление после ошибки записи, продолжение без кнопки перезапуска, таймер и ограничения разных
композиций маскота. Команда работает в чистом clone без локального js/questions.js.
Стиль: шрифт Raleway (Google Fonts), зелёный #005C32, светлые
#E6EFEB/#F0F3F7, радиусы 8–16px. Токены — в :root в css/styles.css.
В репозитории лежит For Google Sheets - AiEmployeeTest.xlsx — скелет базы данных:
все девять листов с правильными заголовками и рабочими значениями Settings,
но без содержимого. Импортируйте его в свою Google-таблицу и заполняйте.
Что внутри и чего нет:
Employees— только заголовки и тестовый профиль; реальных сотрудников и токенов нет;Questions— заголовки и одна строка-пример;Answers— ID вопросов и портретные теги (по ним видно, как размечаются варианты), тексты вариантов убраны. У301оставлен один вариант и значение вНомер правильного ответа— это пример формата, а не настоящий ключ. Строки302–310содержат только ID: если включить такой вопрос, не заполнив варианты, backend откажется выдавать опрос с сообщением «Нет вариантов ответа для включенного вопроса: 302». Либо заполните варианты, либо снимитеВключен, либо удалите строку;Results,Employees_history,Usages,Metrics— заголовки и пояснения;Settings— рабочиеwaveиmin_answers, промпт-заглушка.
Пока листы не заполнены, validateSurvey() будет ругаться на пустые варианты ответов —
это нормально для скелета, проверку имеет смысл запускать уже на своих данных.
Если нужен файл questions.js — пишите на указанную в профиле почту. Если проект оказался Вам полезен, можете поддержать автора (СБП, крипта):
BTC: bc1q3frrup5neh7nhfg944etu2agd4j9u0vg3jyee6
ETH(Arbitrum): 0x43B349d8Cea83215D707EBa3bc35e9917f746b0a
TRX: THSzvy49KNeqRjXsGkurh2A5G4avV4RgN4
XRP: rLWZjS3DMupC4ZdXCX3BVYn4dEtC3iNhgy
SOL: 3xwfybxJ6Tz5t6pjBBkL5yYQCZo6wfbv932UNA4ThdP8
ADA: addr1q926ys75jp5wn2pv32a3t8r8pdhr7w02v0t9j4a8pmg0ruww5rlkctu4lnz2hfcwa5qfn3zhsd0s23r22uqwzx9gu6cq5c4e76
TON: UQC4qlAOD9Nly4K_66GJ_yCsSM3x2sB0vZ2GrBQbc--gZUui
DOGE: DTjNYmbtymzcjUiV4MsZY8MP4dM7MJ6qLC
XMR: 44qRqM6YtnxXUhkgCFqDDrKMPjWriu69FLBoop8Kwp7e1VQsBUJoVQ8JYQjfMV5C6uidTUgSSyoJ65mq8aYG2esZ1rrqfwt
