diff --git a/echo/cypress/cypress.config.js b/echo/cypress/cypress.config.js index c4c5ca05..9f700bc3 100644 --- a/echo/cypress/cypress.config.js +++ b/echo/cypress/cypress.config.js @@ -132,10 +132,16 @@ module.exports = defineConfig({ config.baseUrl = envConfig.dashboardUrl; // Merge the specific environment config to the top level of config.env - // So in tests we can do Cypress.env('auth') or Cypress.env('portalUrl') directly + // So in tests we can do Cypress.env('auth') or Cypress.env('portalUrl') directly. + // Credentials come from env vars first so tracked config stays credential-free. + const envAuth = envConfig.auth || {}; config.env = { ...config.env, ...envConfig, + auth: { + email: process.env.CYPRESS_EMAIL || envAuth.email || "", + password: process.env.CYPRESS_PASSWORD || envAuth.password || "", + }, }; return config; diff --git a/echo/cypress/cypress.env.json b/echo/cypress/cypress.env.json index e2b5ffe7..c2e9650c 100644 --- a/echo/cypress/cypress.env.json +++ b/echo/cypress/cypress.env.json @@ -17,24 +17,33 @@ "dashboardUrl": "https://dashboard.echo-next.dembrane.com/", "portalUrl": "https://portal.echo-next.dembrane.com/", "auth": { - "email": "charugundla.vipul6009@gmail.com", - "password": "test@1234" + "email": "", + "password": "" } }, "prod": { "dashboardUrl": "https://dashboard.echo.dembrane.com/", "portalUrl": "https://portal.echo.dembrane.com/", "auth": { - "email": "charugundla.vipul6009@gmail.com", - "password": "test@1234" + "email": "", + "password": "" } }, "testing": { "dashboardUrl": "https://test.echo.dembrane.com/", "portalUrl": "https://test.portal.echo.dembrane.com/", "auth": { - "email": "charugundla.vipul6009@gmail.com", - "password": "test@1234" + "email": "", + "password": "" + } + }, + "local": { + "dashboardUrl": "http://localhost:5173/", + "portalUrl": "http://localhost:5174/", + "directusUrl": "http://localhost:8055", + "auth": { + "email": "", + "password": "" } } -} \ No newline at end of file +} diff --git a/echo/cypress/e2e/suites/34-live-signposts.cy.js b/echo/cypress/e2e/suites/34-live-signposts.cy.js new file mode 100644 index 00000000..0f428613 --- /dev/null +++ b/echo/cypress/e2e/suites/34-live-signposts.cy.js @@ -0,0 +1,224 @@ +import { loginToApp, logout } from "../../support/functions/login"; +import { openPortalEditor } from "../../support/functions/portal"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Live Signposts", () => { + const directusUrl = (Cypress.env("directusUrl") || "http://localhost:8055").replace( + /\/$/, + "", + ); + let projectId; + let locale = "en-US"; + + const getAuthCredentials = () => { + const auth = Cypress.env("auth") || {}; + if (!auth.email || !auth.password) { + throw new Error( + "Missing Directus credentials. Set CYPRESS_EMAIL and CYPRESS_PASSWORD.", + ); + } + return auth; + }; + + const getFocusTermsTextarea = () => + cy + .get('[data-testid="portal-editor-signposting-focus-terms-textarea"]') + .then(($element) => { + const $textarea = $element.is("textarea") + ? $element + : $element.find("textarea").first(); + return cy.wrap($textarea); + }); + + const toggleLiveSignposting = (enable = true) => { + cy.get('[data-testid="portal-editor-signposting-switch"]') + .scrollIntoView() + .should("exist") + .then(($input) => { + const $label = $input.closest("label"); + const isChecked = $input.is(":checked"); + + if ((enable && !isChecked) || (!enable && isChecked)) { + cy.wrap($label).click({ force: true }); + } + }); + + cy.get('[data-testid="portal-editor-signposting-switch"]').should( + enable ? "be.checked" : "not.be.checked", + ); + }; + + const loginToDirectus = () => { + const auth = getAuthCredentials(); + return cy + .request("POST", `${directusUrl}/auth/login`, { + email: auth.email, + password: auth.password, + }) + .its("body.data.access_token"); + }; + + afterEach(() => { + if (projectId) { + cy.visit(`/${locale}/projects/${projectId}/overview`); + deleteProject(projectId); + } + + cy.get("body").then(($body) => { + const hasSettingsButton = + $body.find('[data-testid="header-settings-gear-button"]:visible').length > 0; + const hasLogoutButton = + $body.find('[data-testid="header-logout-menu-item"]:visible').length > 0; + + if (hasSettingsButton) { + openSettingsMenu(); + logout(); + } else if (hasLogoutButton) { + logout(); + } + }); + + projectId = undefined; + locale = "en-US"; + }); + + const seedConversationWithSignposts = ({ locale, projectId }) => { + const suffix = Cypress._.random(1000, 9999); + const signpostTitle = `Transit affordability ${suffix}`; + const signpostSummary = + "Participants keep returning to the rising cost of buses and trains."; + const signpostQuote = "Public transport is becoming too expensive for families."; + + return loginToDirectus().then((accessToken) => { + const headers = { + Authorization: `Bearer ${accessToken}`, + }; + + return cy + .request({ + body: { + is_finished: false, + participant_name: `Signpost Participant ${suffix}`, + project_id: projectId, + source: "PORTAL_TEXT", + }, + headers, + method: "POST", + url: `${directusUrl}/items/conversation`, + }) + .then((conversationResponse) => { + const conversationId = conversationResponse.body.data.id; + const timestamp = new Date().toISOString(); + + return cy + .request({ + body: { + conversation_id: conversationId, + signpost_processed_at: timestamp, + signpost_ready_at: timestamp, + source: "PORTAL_TEXT", + timestamp, + transcript: + "People agree that public transport costs are rising quickly.", + }, + headers, + method: "POST", + url: `${directusUrl}/items/conversation_chunk`, + }) + .then((chunkResponse) => { + const chunkId = chunkResponse.body.data.id; + + return cy + .request({ + body: { + category: "theme", + confidence: 0.92, + conversation_id: conversationId, + evidence_chunk_id: chunkId, + evidence_quote: signpostQuote, + status: "active", + summary: signpostSummary, + title: signpostTitle, + }, + headers, + method: "POST", + url: `${directusUrl}/items/conversation_signpost`, + }) + .then((signpostResponse) => ({ + conversationId, + locale, + projectId, + signpostId: signpostResponse.body.data.id, + signpostQuote, + signpostSummary, + signpostTitle, + })); + }); + }); + }); + }; + + it("shows seeded signposts in portal settings, conversation overview, and host guide", () => { + loginToApp(); + createProject(); + + cy.location("pathname").then((pathname) => { + const segments = pathname.split("/").filter(Boolean); + projectId = segments[segments.indexOf("projects") + 1]; + locale = segments[0] || locale; + }); + + openPortalEditor(); + toggleLiveSignposting(true); + cy.intercept("PATCH", `${directusUrl}/items/project/*`, (req) => { + const focusTerms = req.body?.signposting_focus_terms; + if ( + typeof focusTerms === "string" && + focusTerms.includes("public transport") + ) { + req.alias = "saveSignpostingFocusTerms"; + } + }); + getFocusTermsTextarea() + .scrollIntoView() + .clear() + .type("affordability{enter}public transport"); + cy.wait("@saveSignpostingFocusTerms"); + + cy.reload(); + openPortalEditor(); + cy.get('[data-testid="portal-editor-signposting-switch"]').should("be.checked"); + getFocusTermsTextarea().should( + "have.value", + "affordability\npublic transport", + ); + + cy.then(() => seedConversationWithSignposts({ locale, projectId })).then( + ({ conversationId, signpostId, signpostQuote, signpostSummary, signpostTitle }) => { + cy.visit( + `/${locale}/projects/${projectId}/conversation/${conversationId}/overview`, + ); + + cy.get('[data-testid="conversation-signposts-section"]', { + timeout: 20000, + }).should("be.visible"); + cy.get(`[data-testid="conversation-signpost-card-${signpostId}"]`) + .should("contain.text", signpostTitle) + .and("contain.text", signpostSummary) + .and("contain.text", signpostQuote); + + cy.visit(`/${locale}/projects/${projectId}/host-guide`); + + cy.get('[data-testid="host-guide-live-signposts-panel"]', { + timeout: 30000, + }).should("be.visible"); + cy.get(`[data-testid="host-guide-live-signpost-${signpostId}"]`, { + timeout: 30000, + }) + .should("contain.text", signpostTitle) + .and("contain.text", signpostSummary); + }, + ); + }); +}); diff --git a/echo/cypress/support/functions/login/index.js b/echo/cypress/support/functions/login/index.js index 4f6f0fa9..a9489010 100644 --- a/echo/cypress/support/functions/login/index.js +++ b/echo/cypress/support/functions/login/index.js @@ -1,9 +1,9 @@ export const loginToApp = () => { cy.log('Logging in with data-testid selectors'); - const user = Cypress.env('auth'); + const user = Cypress.env('auth') || {}; - if (!user || !user.email) { - throw new Error('User credentials not found in environment configuration.'); + if (!user.email || !user.password) { + throw new Error('User credentials not found. Set CYPRESS_EMAIL and CYPRESS_PASSWORD.'); } cy.visit('/'); diff --git a/echo/directus/sync/snapshot/collections/conversation_signpost.json b/echo/directus/sync/snapshot/collections/conversation_signpost.json new file mode 100644 index 00000000..1a8d59c4 --- /dev/null +++ b/echo/directus/sync/snapshot/collections/conversation_signpost.json @@ -0,0 +1,28 @@ +{ + "collection": "conversation_signpost", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "conversation_signpost", + "color": null, + "display_template": "{{title}}", + "group": null, + "hidden": false, + "icon": null, + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": 14, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + }, + "schema": { + "name": "conversation_signpost" + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation/signposts.json b/echo/directus/sync/snapshot/fields/conversation/signposts.json new file mode 100644 index 00000000..bc1b4ea6 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation/signposts.json @@ -0,0 +1,28 @@ +{ + "collection": "conversation", + "field": "signposts", + "type": "alias", + "meta": { + "collection": "conversation", + "conditions": null, + "display": null, + "display_options": null, + "field": "signposts", + "group": null, + "hidden": false, + "interface": "list-o2m", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 28, + "special": [ + "o2m" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_chunk/signpost_processed_at.json b/echo/directus/sync/snapshot/fields/conversation_chunk/signpost_processed_at.json new file mode 100644 index 00000000..4701c6a8 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_chunk/signpost_processed_at.json @@ -0,0 +1,44 @@ +{ + "collection": "conversation_chunk", + "field": "signpost_processed_at", + "type": "timestamp", + "meta": { + "collection": "conversation_chunk", + "conditions": null, + "display": null, + "display_options": null, + "field": "signpost_processed_at", + "group": null, + "hidden": true, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": false, + "sort": 26, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "signpost_processed_at", + "table": "conversation_chunk", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_chunk/signpost_ready_at.json b/echo/directus/sync/snapshot/fields/conversation_chunk/signpost_ready_at.json new file mode 100644 index 00000000..241dc944 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_chunk/signpost_ready_at.json @@ -0,0 +1,44 @@ +{ + "collection": "conversation_chunk", + "field": "signpost_ready_at", + "type": "timestamp", + "meta": { + "collection": "conversation_chunk", + "conditions": null, + "display": null, + "display_options": null, + "field": "signpost_ready_at", + "group": null, + "hidden": false, + "interface": "datetime", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 25, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "signpost_ready_at", + "table": "conversation_chunk", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/category.json b/echo/directus/sync/snapshot/fields/conversation_signpost/category.json new file mode 100644 index 00000000..c88c8dc6 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/category.json @@ -0,0 +1,63 @@ +{ + "collection": "conversation_signpost", + "field": "category", + "type": "string", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "category", + "group": null, + "hidden": false, + "interface": "select-dropdown", + "note": null, + "options": { + "choices": [ + { + "text": "Agreement", + "value": "agreement" + }, + { + "text": "Disagreement", + "value": "disagreement" + }, + { + "text": "Tension", + "value": "tension" + }, + { + "text": "Theme", + "value": "theme" + } + ] + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 6, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "category", + "table": "conversation_signpost", + "data_type": "character varying", + "default_value": null, + "max_length": 32, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/confidence.json b/echo/directus/sync/snapshot/fields/conversation_signpost/confidence.json new file mode 100644 index 00000000..e0a1450e --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/confidence.json @@ -0,0 +1,44 @@ +{ + "collection": "conversation_signpost", + "field": "confidence", + "type": "float", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "confidence", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 11, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "confidence", + "table": "conversation_signpost", + "data_type": "real", + "default_value": null, + "max_length": null, + "numeric_precision": 24, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/conversation_id.json b/echo/directus/sync/snapshot/fields/conversation_signpost/conversation_id.json new file mode 100644 index 00000000..5c097a52 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/conversation_id.json @@ -0,0 +1,49 @@ +{ + "collection": "conversation_signpost", + "field": "conversation_id", + "type": "uuid", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "conversation_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "enableLink": true, + "template": "{{participant_name}}.{{project_id.name}}" + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 4, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "conversation_id", + "table": "conversation_signpost", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "conversation", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/created_at.json b/echo/directus/sync/snapshot/fields/conversation_signpost/created_at.json new file mode 100644 index 00000000..c44abeac --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/created_at.json @@ -0,0 +1,46 @@ +{ + "collection": "conversation_signpost", + "field": "created_at", + "type": "timestamp", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "created_at", + "group": null, + "hidden": false, + "interface": null, + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 2, + "special": [ + "date-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "created_at", + "table": "conversation_signpost", + "data_type": "timestamp with time zone", + "default_value": "CURRENT_TIMESTAMP", + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/evidence_chunk_id.json b/echo/directus/sync/snapshot/fields/conversation_signpost/evidence_chunk_id.json new file mode 100644 index 00000000..19f7a528 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/evidence_chunk_id.json @@ -0,0 +1,49 @@ +{ + "collection": "conversation_signpost", + "field": "evidence_chunk_id", + "type": "uuid", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "evidence_chunk_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "enableLink": true, + "template": "{{timestamp}}" + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 5, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "evidence_chunk_id", + "table": "conversation_signpost", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "conversation_chunk", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/evidence_quote.json b/echo/directus/sync/snapshot/fields/conversation_signpost/evidence_quote.json new file mode 100644 index 00000000..4804aae7 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/evidence_quote.json @@ -0,0 +1,44 @@ +{ + "collection": "conversation_signpost", + "field": "evidence_quote", + "type": "text", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "evidence_quote", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 9, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "evidence_quote", + "table": "conversation_signpost", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/id.json b/echo/directus/sync/snapshot/fields/conversation_signpost/id.json new file mode 100644 index 00000000..f16f7d36 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/id.json @@ -0,0 +1,46 @@ +{ + "collection": "conversation_signpost", + "field": "id", + "type": "uuid", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "id", + "group": null, + "hidden": false, + "interface": null, + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 1, + "special": [ + "uuid" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "id", + "table": "conversation_signpost", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": true, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/status.json b/echo/directus/sync/snapshot/fields/conversation_signpost/status.json new file mode 100644 index 00000000..82c797db --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/status.json @@ -0,0 +1,55 @@ +{ + "collection": "conversation_signpost", + "field": "status", + "type": "string", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "status", + "group": null, + "hidden": false, + "interface": "select-dropdown", + "note": null, + "options": { + "choices": [ + { + "text": "Active", + "value": "active" + }, + { + "text": "Resolved", + "value": "resolved" + } + ] + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 10, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "status", + "table": "conversation_signpost", + "data_type": "character varying", + "default_value": "active", + "max_length": 32, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/summary.json b/echo/directus/sync/snapshot/fields/conversation_signpost/summary.json new file mode 100644 index 00000000..0094c640 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/summary.json @@ -0,0 +1,44 @@ +{ + "collection": "conversation_signpost", + "field": "summary", + "type": "text", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "summary", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 8, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "summary", + "table": "conversation_signpost", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/title.json b/echo/directus/sync/snapshot/fields/conversation_signpost/title.json new file mode 100644 index 00000000..058ca8a4 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/title.json @@ -0,0 +1,44 @@ +{ + "collection": "conversation_signpost", + "field": "title", + "type": "text", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "title", + "group": null, + "hidden": false, + "interface": null, + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 7, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "title", + "table": "conversation_signpost", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/conversation_signpost/updated_at.json b/echo/directus/sync/snapshot/fields/conversation_signpost/updated_at.json new file mode 100644 index 00000000..b002e962 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation_signpost/updated_at.json @@ -0,0 +1,46 @@ +{ + "collection": "conversation_signpost", + "field": "updated_at", + "type": "timestamp", + "meta": { + "collection": "conversation_signpost", + "conditions": null, + "display": null, + "display_options": null, + "field": "updated_at", + "group": null, + "hidden": false, + "interface": null, + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 3, + "special": [ + "date-updated" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "updated_at", + "table": "conversation_signpost", + "data_type": "timestamp with time zone", + "default_value": "CURRENT_TIMESTAMP", + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project/is_signposting_enabled.json b/echo/directus/sync/snapshot/fields/project/is_signposting_enabled.json new file mode 100644 index 00000000..940e3ec2 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project/is_signposting_enabled.json @@ -0,0 +1,46 @@ +{ + "collection": "project", + "field": "is_signposting_enabled", + "type": "boolean", + "meta": { + "collection": "project", + "conditions": null, + "display": null, + "display_options": null, + "field": "is_signposting_enabled", + "group": null, + "hidden": false, + "interface": "boolean", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 34, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "is_signposting_enabled", + "table": "project", + "data_type": "boolean", + "default_value": false, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project/signposting_focus_terms.json b/echo/directus/sync/snapshot/fields/project/signposting_focus_terms.json new file mode 100644 index 00000000..42b4dcf0 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project/signposting_focus_terms.json @@ -0,0 +1,47 @@ +{ + "collection": "project", + "field": "signposting_focus_terms", + "type": "text", + "meta": { + "collection": "project", + "conditions": null, + "display": null, + "display_options": null, + "field": "signposting_focus_terms", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": { + "clear": true, + "trim": true + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 35, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "signposting_focus_terms", + "table": "project", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/relations/conversation_signpost/conversation_id.json b/echo/directus/sync/snapshot/relations/conversation_signpost/conversation_id.json new file mode 100644 index 00000000..08e7aded --- /dev/null +++ b/echo/directus/sync/snapshot/relations/conversation_signpost/conversation_id.json @@ -0,0 +1,25 @@ +{ + "collection": "conversation_signpost", + "field": "conversation_id", + "related_collection": "conversation", + "meta": { + "junction_field": null, + "many_collection": "conversation_signpost", + "many_field": "conversation_id", + "one_allowed_collections": null, + "one_collection": "conversation", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": "signposts", + "sort_field": null + }, + "schema": { + "table": "conversation_signpost", + "column": "conversation_id", + "foreign_key_table": "conversation", + "foreign_key_column": "id", + "constraint_name": "conversation_signpost_conversation_id_foreign", + "on_update": "NO ACTION", + "on_delete": "CASCADE" + } +} diff --git a/echo/directus/sync/snapshot/relations/conversation_signpost/evidence_chunk_id.json b/echo/directus/sync/snapshot/relations/conversation_signpost/evidence_chunk_id.json new file mode 100644 index 00000000..e6d6fb4d --- /dev/null +++ b/echo/directus/sync/snapshot/relations/conversation_signpost/evidence_chunk_id.json @@ -0,0 +1,25 @@ +{ + "collection": "conversation_signpost", + "field": "evidence_chunk_id", + "related_collection": "conversation_chunk", + "meta": { + "junction_field": null, + "many_collection": "conversation_signpost", + "many_field": "evidence_chunk_id", + "one_allowed_collections": null, + "one_collection": "conversation_chunk", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "conversation_signpost", + "column": "evidence_chunk_id", + "foreign_key_table": "conversation_chunk", + "foreign_key_column": "id", + "constraint_name": "conversation_signpost_evidence_chunk_id_foreign", + "on_update": "NO ACTION", + "on_delete": "SET NULL" + } +} diff --git a/echo/frontend/src/components/conversation/ConversationSignpostsSection.tsx b/echo/frontend/src/components/conversation/ConversationSignpostsSection.tsx new file mode 100644 index 00000000..546b8ab8 --- /dev/null +++ b/echo/frontend/src/components/conversation/ConversationSignpostsSection.tsx @@ -0,0 +1,154 @@ +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; +import { Badge, Group, Paper, Stack, Text, Title } from "@mantine/core"; +import { formatDistance } from "date-fns"; +import { testId } from "@/lib/testUtils"; + +const SIGNPOST_CATEGORY_ORDER: Array< + NonNullable +> = ["agreement", "disagreement", "tension", "theme"]; + +const getCategoryColor = (category: ConversationSignpost["category"]) => { + switch (category) { + case "agreement": + return "teal"; + case "disagreement": + return "red"; + case "tension": + return "orange"; + case "theme": + default: + return "blue"; + } +}; + +const getCategoryLabel = (category: ConversationSignpost["category"]) => { + switch (category) { + case "agreement": + return ( + Agreement + ); + case "disagreement": + return ( + + Disagreement + + ); + case "tension": + return Tension; + case "theme": + default: + return Theme; + } +}; + +const getUpdatedLabel = (updatedAt: string | null) => { + if (!updatedAt) { + return t`Updated just now`; + } + + const date = new Date(updatedAt); + if (Number.isNaN(date.getTime())) { + return t`Updated just now`; + } + + return t`Updated ${formatDistance(date, new Date(), { addSuffix: true })}`; +}; + +type ConversationSignpostsSectionProps = { + signposts: ConversationSignpost[]; +}; + +export const ConversationSignpostsSection = ({ + signposts, +}: ConversationSignpostsSectionProps) => { + const activeSignposts = signposts.filter((signpost) => signpost.status === "active"); + + if (activeSignposts.length === 0) { + return null; + } + + return ( + + + + <Trans>Signposts</Trans> + + + + Live themes, agreements, disagreements, and tensions surfaced + from the latest transcript chunks. + + + + + {SIGNPOST_CATEGORY_ORDER.map((category) => { + const items = activeSignposts + .filter((signpost) => signpost.category === category) + .sort((left, right) => { + const leftTime = new Date( + left.updated_at ?? left.created_at ?? 0, + ).getTime(); + const rightTime = new Date( + right.updated_at ?? right.created_at ?? 0, + ).getTime(); + return rightTime - leftTime; + }); + + if (items.length === 0) { + return null; + } + + return ( + + + + {getCategoryLabel(category)} + + + {t`${items.length} live`} + + + + {items.map((signpost) => ( + + + + {signpost.title} + + {getUpdatedLabel(signpost.updated_at)} + + + {signpost.summary && ( + {signpost.summary} + )} + {signpost.evidence_quote && ( + + "{signpost.evidence_quote}" + + )} + + + ))} + + ); + })} + + ); +}; diff --git a/echo/frontend/src/components/conversation/hooks/index.ts b/echo/frontend/src/components/conversation/hooks/index.ts index 76f9946c..39557abe 100644 --- a/echo/frontend/src/components/conversation/hooks/index.ts +++ b/echo/frontend/src/components/conversation/hooks/index.ts @@ -882,6 +882,21 @@ export const useConversationById = ({ }, ], }, + { + signposts: [ + "id", + "conversation_id", + "category", + "title", + "summary", + "evidence_quote", + "status", + "confidence", + "created_at", + "updated_at", + "evidence_chunk_id", + ], + }, ...(loadConversationChunks ? [ { diff --git a/echo/frontend/src/components/project/ProjectPortalEditor.tsx b/echo/frontend/src/components/project/ProjectPortalEditor.tsx index 87064720..c0c97e21 100644 --- a/echo/frontend/src/components/project/ProjectPortalEditor.tsx +++ b/echo/frontend/src/components/project/ProjectPortalEditor.tsx @@ -78,9 +78,11 @@ const FormSchema = z.object({ get_reply_prompt: z.string(), is_get_reply_enabled: z.boolean(), is_project_notification_subscription_allowed: z.boolean(), + is_signposting_enabled: z.boolean(), is_verify_enabled: z.boolean(), is_verify_on_finish_enabled: z.boolean(), language: z.enum(["en", "nl", "de", "fr", "es", "it"]), + signposting_focus_terms: z.string(), verification_topics: z.array(z.string()), }); @@ -300,9 +302,11 @@ const ProjectPortalEditorComponent: React.FC = ({ is_get_reply_enabled: project.is_get_reply_enabled ?? false, is_project_notification_subscription_allowed: project.is_project_notification_subscription_allowed ?? false, + is_signposting_enabled: project.is_signposting_enabled ?? false, is_verify_enabled: project.is_verify_enabled ?? false, is_verify_on_finish_enabled: project.is_verify_on_finish_enabled ?? false, language: projectLanguageCode, + signposting_focus_terms: project.signposting_focus_terms ?? "", verification_topics: selectedTopicDefaults, }; }, [project.id, projectLanguageCode, selectedTopicDefaults]); @@ -340,6 +344,11 @@ const ProjectPortalEditorComponent: React.FC = ({ name: "is_verify_enabled", }); + const watchedSignpostingEnabled = useWatch({ + control, + name: "is_signposting_enabled", + }); + const watchedAskForEmail = useWatch({ control, name: "default_conversation_ask_for_participant_email", @@ -1454,6 +1463,80 @@ const ProjectPortalEditorComponent: React.FC = ({ /> + + + + <Trans>Signposts</Trans> + + + + + + Generate live host-facing signposts from each + conversation as transcripts arrive. In v1, signposts are + grouped into agreement, disagreement, tension, and + theme. + + + ( + + } + checked={field.value} + onChange={(e) => + field.onChange(e.currentTarget.checked) + } + {...testId("portal-editor-signposting-switch")} + /> + )} + /> + ( +