This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
|
||||
// The notes page's way into the feedback survey (feedback.guide.ts): a button that asks the guides
|
||||
// module to start `notes.feedback` through the shared surface coordinator (`guides.start`, joint
|
||||
// contract §12), the same command the support chat's help offers send. It shows only while the guides
|
||||
// module serves that command, so an app without guides renders nothing here; `kit template:prune
|
||||
// --without guides` removes it with this folder.
|
||||
|
||||
import type { SurfaceCoordinator } from "@conusai-kit/core"
|
||||
import { useSurfaceCoordinator } from "@conusai-kit/ui"
|
||||
import { type ReactElement, useState, useSyncExternalStore } from "react"
|
||||
import type { FeedbackButtonLabels } from "./feedback-labels.js"
|
||||
|
||||
const never = () => () => undefined
|
||||
|
||||
export function FeedbackButton({ labels }: { labels: FeedbackButtonLabels }): ReactElement | null {
|
||||
const coordinator = useSurfaceCoordinator<SurfaceCoordinator>()
|
||||
const served = useSyncExternalStore(
|
||||
coordinator ? (listener) => coordinator.subscribe(listener) : never,
|
||||
() => coordinator?.serves("guides.start") ?? false,
|
||||
() => false,
|
||||
)
|
||||
const [unavailable, setUnavailable] = useState(false)
|
||||
if (!coordinator || !served) return null
|
||||
const start = async () => {
|
||||
const result = await coordinator.dispatch({
|
||||
version: 1,
|
||||
requestId: globalThis.crypto.randomUUID(),
|
||||
type: "guides.start",
|
||||
guideKey: "notes.feedback",
|
||||
})
|
||||
setUnavailable(result.status !== "ok")
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
<button type="button" onClick={() => void start()}>
|
||||
{labels.open}
|
||||
</button>{" "}
|
||||
<span role="status">{unavailable ? labels.unavailable : ""}</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// The feedback button's labels, resolved on the server: the notes page is a server component and
|
||||
// cannot call a function of feedback-button.tsx, which is a client module (Next refuses it with
|
||||
// "Attempted to call feedbackLabels() from the server"). `kit template:prune --without guides`
|
||||
// removes it with this folder.
|
||||
|
||||
export interface FeedbackButtonLabels {
|
||||
readonly open: string
|
||||
/** Said when the survey cannot start: already answered, or not served to this person. */
|
||||
readonly unavailable: string
|
||||
}
|
||||
|
||||
type Translate = (key: string) => string
|
||||
|
||||
export function feedbackLabels(t: Translate): FeedbackButtonLabels {
|
||||
return { open: t("feedback.open"), unavailable: t("feedback.unavailable") }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// The example code survey of the template (ADR-046): a short product-feedback questionnaire about
|
||||
// notes, published at deploy with the module's other code guides. It starts only when the person asks
|
||||
// for it -- the "Give feedback" button of the notes page (feedback-button.tsx) or the help offers of
|
||||
// another module (the support chat lists it by its title) -- and runs once per person.
|
||||
//
|
||||
// Answers are stored per guide version on the platform and read in the console (Guides, the survey,
|
||||
// Results); the free-text question shows a fixed hint not to enter personal data. The optional
|
||||
// `screenshot` question takes up to three images or PDFs, each up to the project's survey file size
|
||||
// (`max_file_size_mb: null`: data_policy.survey_file_max_mb, 10 MB unless configured), with the fixed
|
||||
// hint that files must not contain personal data such as identity documents; the files go through
|
||||
// this app's own server (guides.runtime.uploadFile) to the project's Blob store, and the console's
|
||||
// Results list them. The project's guide settings must allow the `survey` kind (console, Guides,
|
||||
// Settings) before it is served.
|
||||
|
||||
import { defineGuide } from "@conusai-kit/guides"
|
||||
import { guideText } from "./messages.js"
|
||||
|
||||
const t = guideText.survey
|
||||
|
||||
export const feedbackSurvey = defineGuide({
|
||||
key: "notes.feedback",
|
||||
schema_version: 1,
|
||||
kind: "survey",
|
||||
default_locale: "en",
|
||||
audience: { trait: { name: "role", in: ["owner", "admin", "member"] } },
|
||||
trigger: { manual: true },
|
||||
frequency: "once",
|
||||
survey: {
|
||||
title: t.title,
|
||||
intro: t.intro,
|
||||
questions: [
|
||||
{
|
||||
type: "rating",
|
||||
id: "ease",
|
||||
prompt: t.ease,
|
||||
required: true,
|
||||
scale: "1-5",
|
||||
low_label: t.easeLow,
|
||||
high_label: t.easeHigh,
|
||||
},
|
||||
{
|
||||
type: "single_choice",
|
||||
id: "use",
|
||||
prompt: t.use,
|
||||
required: false,
|
||||
options: [
|
||||
{ id: "meetings", label: t.useMeetings },
|
||||
{ id: "ideas", label: t.useIdeas },
|
||||
{ id: "tasks", label: t.useTasks },
|
||||
],
|
||||
allow_other: true,
|
||||
},
|
||||
{ type: "text", id: "improve", prompt: t.improve, required: false, multiline: true },
|
||||
{
|
||||
type: "file",
|
||||
id: "screenshot",
|
||||
prompt: t.screenshot,
|
||||
required: false,
|
||||
accept: ["image", "pdf"],
|
||||
max_files: 3,
|
||||
max_file_size_mb: null,
|
||||
},
|
||||
],
|
||||
submit_label: null,
|
||||
thank_you: t.thankYou,
|
||||
allow_dismiss: true,
|
||||
one_question_per_screen: true,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
// The example code tour of the template (in-app guides spec §7.3): defined next to the module it
|
||||
// explains, published to the deployment's environment by the deploy step, and run from memory with
|
||||
// `pnpm dev` (the fake platform). Its steps point at the `data-guide` anchors of ui/pages.tsx and
|
||||
// ui/note-form.tsx; `kit check` warns when one of them is removed.
|
||||
//
|
||||
// The audience is a trait the app sends (`role`, from the guides module): a viewer cannot add a note,
|
||||
// so the tour is for the members who can. The platform compares only the traits the project's guide
|
||||
// settings allow-list, so allow `role` there (console, Guides, Settings) before the tour shows.
|
||||
|
||||
import { defineGuide } from "@conusai-kit/guides"
|
||||
import { guideText } from "./messages.js"
|
||||
|
||||
const t = guideText.tour
|
||||
|
||||
export const firstNoteTour = defineGuide({
|
||||
key: "notes.first-note",
|
||||
schema_version: 1,
|
||||
kind: "tour",
|
||||
default_locale: "en",
|
||||
audience: { trait: { name: "role", in: ["owner", "admin", "member"] } },
|
||||
trigger: { page: "/:org/:workspace/:project/notes" },
|
||||
frequency: "once",
|
||||
steps: [
|
||||
{
|
||||
id: "new",
|
||||
type: "tooltip",
|
||||
target: { anchor: "notes.new-note" },
|
||||
title: t.newTitle,
|
||||
body: t.newBody,
|
||||
advance: { on: "next_button" },
|
||||
},
|
||||
{
|
||||
id: "title",
|
||||
type: "tooltip",
|
||||
target: { anchor: "notes.note-title" },
|
||||
title: t.titleTitle,
|
||||
body: t.titleBody,
|
||||
advance: { on: "next_button" },
|
||||
},
|
||||
{
|
||||
id: "list",
|
||||
type: "tooltip",
|
||||
target: { anchor: "notes.note-list" },
|
||||
title: t.listTitle,
|
||||
body: t.listBody,
|
||||
advance: { on: "next_button" },
|
||||
},
|
||||
{ id: "done", type: "modal", title: t.doneTitle, body: t.doneBody },
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
// The example checklist of the template: one task completes with the tour, one when the app reports
|
||||
// `note_created` (the note form emits it, `emitAppEvent` of @conusai-kit/ui, once the note is saved),
|
||||
// and one the person ticks.
|
||||
|
||||
import { defineGuide } from "@conusai-kit/guides"
|
||||
import { guideText } from "./messages.js"
|
||||
|
||||
const t = guideText.checklist
|
||||
|
||||
export const gettingStartedChecklist = defineGuide({
|
||||
key: "notes.getting-started",
|
||||
schema_version: 1,
|
||||
kind: "checklist",
|
||||
default_locale: "en",
|
||||
audience: { trait: { name: "role", in: ["owner", "admin", "member"] } },
|
||||
trigger: { page: "/:org/:workspace/:project/notes" },
|
||||
frequency: "until_completed",
|
||||
title: t.title,
|
||||
tasks: [
|
||||
{
|
||||
id: "tour",
|
||||
title: t.tour,
|
||||
guide: "notes.first-note",
|
||||
condition: { type: "guide_completed", guide: "notes.first-note" },
|
||||
},
|
||||
{ id: "create", title: t.create, condition: { type: "event", name: "note_created" } },
|
||||
{ id: "share", title: t.share, condition: { type: "manual" } },
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
// What the notes module contributes to the guides module: its code guides, published at deploy. The
|
||||
// guides module is opt-in: `kit template:prune --without guides` removes this folder and the lines
|
||||
// marked `kit:guides` that import it.
|
||||
//
|
||||
// A module contributes audience traits the same way, to `guidesTraits` ({ key, resolve }); the
|
||||
// guides module itself contributes `role`, which the guides below use.
|
||||
|
||||
import { contribute } from "@conusai-kit/core"
|
||||
import { guidesCode } from "@conusai-kit/guides"
|
||||
import { feedbackSurvey } from "./feedback.guide.js"
|
||||
import { firstNoteTour } from "./first-note.guide.js"
|
||||
import { gettingStartedChecklist } from "./getting-started.guide.js"
|
||||
|
||||
export const NOTES_GUIDES = [contribute(guidesCode, [firstNoteTour, gettingStartedChecklist, feedbackSurvey])]
|
||||
@@ -0,0 +1,55 @@
|
||||
// The words of the notes module's guides, in both languages (spec §7.3: a guide's strings come from
|
||||
// the module's catalogue). A guide's text is per locale, so each message here is one object of both.
|
||||
|
||||
const both = (en: string, lt: string) => ({ en, lt })
|
||||
|
||||
export const guideText = {
|
||||
tour: {
|
||||
newTitle: both("Write a note", "Parašykite pastabą"),
|
||||
newBody: both(
|
||||
"Give the note a title and some text, then add it. Notes belong to this project.",
|
||||
"Suteikite pastabai pavadinimą ir tekstą, tada pridėkite. Pastabos priklauso šiam projektui.",
|
||||
),
|
||||
titleTitle: both("A short title", "Trumpas pavadinimas"),
|
||||
titleBody: both(
|
||||
"The title is what the list shows. Up to 200 characters.",
|
||||
"Pavadinimą rodo sąrašas. Iki 200 simbolių.",
|
||||
),
|
||||
listTitle: both("Your notes", "Jūsų pastabos"),
|
||||
listBody: both(
|
||||
"Every note of the project is here. Open one to edit it or attach files.",
|
||||
"Čia yra visos projekto pastabos. Atidarykite pastabą, kad ją redaguotumėte ar pridėtumėte failų.",
|
||||
),
|
||||
doneTitle: both("That is all", "Tai viskas"),
|
||||
doneBody: both(
|
||||
"You can run this tour again from the help menu.",
|
||||
"Šį turą galite paleisti dar kartą iš pagalbos meniu.",
|
||||
),
|
||||
},
|
||||
checklist: {
|
||||
title: both("Get started with notes", "Pradėkite naudotis pastabomis"),
|
||||
tour: both("Take the notes tour", "Peržiūrėkite pastabų turą"),
|
||||
create: both("Add your first note", "Pridėkite pirmąją pastabą"),
|
||||
share: both("Tell your team where the notes are", "Praneškite komandai, kur yra pastabos"),
|
||||
},
|
||||
survey: {
|
||||
title: both("How are notes working for you?", "Kaip jums sekasi su pastabomis?"),
|
||||
intro: both(
|
||||
"Four short questions help us improve notes. It takes about a minute.",
|
||||
"Keturi trumpi klausimai padės mums patobulinti pastabas. Tai užtruks apie minutę.",
|
||||
),
|
||||
ease: both("How easy are notes to use?", "Kiek lengva naudotis pastabomis?"),
|
||||
easeLow: both("Very hard", "Labai sunku"),
|
||||
easeHigh: both("Very easy", "Labai lengva"),
|
||||
use: both("What do you mostly use notes for?", "Kam dažniausiai naudojate pastabas?"),
|
||||
useMeetings: both("Meeting notes", "Susitikimų užrašams"),
|
||||
useIdeas: both("Ideas", "Idėjoms"),
|
||||
useTasks: both("Tasks and to-dos", "Užduotims"),
|
||||
improve: both("What would make notes better for you?", "Kas padarytų pastabas jums geresnes?"),
|
||||
screenshot: both(
|
||||
"Attach a screenshot or a PDF of what got in your way, if you like.",
|
||||
"Jei norite, pridėkite ekrano kopiją ar PDF failą su tuo, kas jums trukdė.",
|
||||
),
|
||||
thankYou: both("Thank you. We read every answer.", "Ačiū. Perskaitome kiekvieną atsakymą."),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user