Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/dist/roundtable.css

Large diffs are not rendered by default.

44 changes: 22 additions & 22 deletions assets/dist/roundtable.js

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions assets/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ export function publishCase(body) {
return post('/publish', body);
}

/**
* Submit a topic directly to hub moderation, bypassing chat/AI drafting.
* @param {{type:string, title:string, body:string}} payload
* @returns {Promise<{case:object}|{error:{kind:string}}>}
*/
export function submitTopic(payload) {
return post('/topics', payload);
}

/**
* List comments on a public topic.
* @param {string} caseId
Expand Down
14 changes: 7 additions & 7 deletions assets/src/comment-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { h } from 'preact';
import { useRef, useState } from 'preact/hooks';
import { renderMarkdown } from './markdown.js';
import { PersonAvatar } from './avatars.jsx';

const TOOLS = [
{ id: 'bold', title: 'Bold', wrap: ['**', '**'], label: <b>B</b> },
Expand Down Expand Up @@ -62,9 +61,11 @@ function applyPrefix(el, prefix, value) {

/**
* Full markdown comment composer with Write/Preview and toolbar.
* @param {{value:string, onInput:(v:string)=>void, onSubmit:()=>void, posting:boolean, error:boolean}} props
* @param {{value:string, onInput:(v:string)=>void, onSubmit:()=>void, posting:boolean, error:boolean, submitDisabled?:boolean}} props
*/
export function CommentEditor({ value, onInput, onSubmit, posting, error }) {
export function CommentEditor({
value, onInput, onSubmit, posting, error, submitDisabled = false,
}) {
const [mode, setMode] = useState('write');
const areaRef = useRef(null);

Expand All @@ -84,16 +85,15 @@ export function CommentEditor({ value, onInput, onSubmit, posting, error }) {
}

function onKeyDown(e) {
if (mode !== 'write' || posting) return;
if (mode !== 'write' || posting || submitDisabled) return;
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
if (value.trim()) onSubmit();
}
}

return (
<div class="rt-addwrap rt-cm-composer">
<PersonAvatar initials="Y" size="sm" class="rt-you-ava" />
<div class="rt-cm-composer">
<div class="rt-editor rt-mdeditor">
<div class="rt-ed-top">
<div class="rt-ed-tabs">
Expand Down Expand Up @@ -135,7 +135,7 @@ export function CommentEditor({ value, onInput, onSubmit, posting, error }) {
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3ZM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6Zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5Z" /></svg>
</span>
{error ? <span class="rt-ed-error">Could not post. Try again.</span> : <span class="rt-ed-spacer" />}
<button type="button" class="rt-post-btn" disabled={posting || !value.trim() || mode !== 'write'} onClick={onSubmit}>
<button type="button" class="rt-post-btn" disabled={posting || !value.trim() || mode !== 'write' || submitDisabled} onClick={onSubmit}>
{posting ? 'Posting…' : 'Comment'}
</button>
</div>
Expand Down
118 changes: 78 additions & 40 deletions assets/src/community-app.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jsx h */
import { h } from 'preact';
import { Fragment, h } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { ChatPanel } from './components.jsx';
import { AgentAvatar } from './avatars.jsx';
Expand All @@ -9,9 +9,11 @@ import { RoadmapList } from './roadmap-list.jsx';
import { ParticipatingList } from './participating-list.jsx';
import { TopicDetail } from './topic-detail.jsx';
import { PublishDialog } from './publish-dialog.jsx';
import { publishCase } from './api.js';
import { publishCase, submitTopic } from './api.js';
import { fetchTabCounts } from './tab-counts.js';
import { agentDisplayName, betaEnabled, projectDisplayName } from './config.js';
import {
agentDisplayName, betaEnabled, chatEnabled, projectDisplayName,
} from './config.js';

const TABS = [
{ id: 'all', label: 'All', enabled: true },
Expand All @@ -30,6 +32,10 @@ export function CommunityApp() {
const [publishBusy, setPublishBusy] = useState(false);
const [tabCounts, setTabCounts] = useState(null);
const [chatOpen, setChatOpen] = useState(false);
const [newTopicOpen, setNewTopicOpen] = useState(false);
const [newTopicBusy, setNewTopicBusy] = useState(false);
const [newTopicErrorKind, setNewTopicErrorKind] = useState(null);
const chatOn = chatEnabled();
const agentName = agentDisplayName();
const projectName = projectDisplayName();

Expand All @@ -43,8 +49,17 @@ export function CommunityApp() {
}, [listRefresh]);

const requestNewTopic = () => {
setResetNonce((n) => n + 1);
setChatOpen(true);
if (chatOn) {
setResetNonce((n) => n + 1);
setChatOpen(true);
} else {
setNewTopicErrorKind(null);
setNewTopicOpen(true);
}
};
const closeNewTopic = () => {
setNewTopicOpen(false);
setNewTopicErrorKind(null);
};
const openTopic = (topic) => {
setSelectedTopic(topic);
Expand All @@ -71,8 +86,23 @@ export function CommunityApp() {
if (activeTab !== 'started') setActiveTab('started');
}

async function confirmDirectSubmit({ title, summary, type }) {
if (newTopicBusy) return;
setNewTopicBusy(true);
setNewTopicErrorKind(null);
const res = await submitTopic({ type, title, body: summary });
setNewTopicBusy(false);
if (res.error) {
setNewTopicErrorKind(res.error.kind);
return;
}
setNewTopicOpen(false);
bumpLists();
setActiveTab('started');
}

return (
<div class={'rt-layout' + (chatOpen ? ' rt-chat-open' : '')}>
<div class={'rt-layout' + (chatOn && chatOpen ? ' rt-chat-open' : '')}>
<main class="rt-main">
{view === 'detail' && selectedTopic ? (
<TopicDetail topic={selectedTopic} onBack={backToList} onVoteChange={refreshTabCountsOnly} />
Expand Down Expand Up @@ -103,11 +133,6 @@ export function CommunityApp() {
</button>
))}
</nav>
{activeTab === 'all' && (
<div class="rt-banner">
<b>Public &amp; anonymous.</b> Only community handles are shown, never real names.
</div>
)}
{activeTab === 'all' && (
<TopicList refreshNonce={listRefresh} onSelectTopic={openTopic} onVoteChange={refreshTabCountsOnly} />
)}
Expand All @@ -128,36 +153,40 @@ export function CommunityApp() {
</div>
)}
</main>
{!chatOpen && (
<button
type="button"
class="rt-chat-launch"
aria-label={`Open ${agentName} chat`}
onClick={() => setChatOpen(true)}
>
<AgentAvatar size="sm" />
</button>
{chatOn && (
<Fragment>
{!chatOpen && (
<button
type="button"
class="rt-chat-launch"
aria-label={`Open ${agentName} chat`}
onClick={() => setChatOpen(true)}
>
<AgentAvatar size="sm" />
</button>
)}
{chatOpen && (
<button
type="button"
class="rt-chat-backdrop"
aria-label="Close chat"
onClick={() => setChatOpen(false)}
/>
)}
<aside class={'rt-aside' + (chatOpen ? ' is-open' : '')}>
<button
type="button"
class="rt-chat-close"
aria-label="Close chat"
onClick={() => setChatOpen(false)}
>×</button>
<ChatPanel
resetNonce={resetNonce}
onTopicPublished={() => { bumpLists(); setActiveTab('started'); }}
/>
</aside>
</Fragment>
)}
{chatOpen && (
<button
type="button"
class="rt-chat-backdrop"
aria-label="Close chat"
onClick={() => setChatOpen(false)}
/>
)}
<aside class={'rt-aside' + (chatOpen ? ' is-open' : '')}>
<button
type="button"
class="rt-chat-close"
aria-label="Close chat"
onClick={() => setChatOpen(false)}
>×</button>
<ChatPanel
resetNonce={resetNonce}
onTopicPublished={() => { bumpLists(); setActiveTab('started'); }}
/>
</aside>
{publishDraft && (
<PublishDialog
draft={publishDraft}
Expand All @@ -166,6 +195,15 @@ export function CommunityApp() {
onPublish={confirmPublish}
/>
)}
{newTopicOpen && (
<PublishDialog
variant="direct"
busy={newTopicBusy}
errorKind={newTopicErrorKind}
onCancel={closeNewTopic}
onPublish={confirmDirectSubmit}
/>
)}
</div>
);
}
12 changes: 11 additions & 1 deletion assets/src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// assets/src/config.js — the names localized by src/Admin/Page.php.

/** @returns {{agentName?:string, projectName?:string, initialMessage?:string, beta?:boolean, chatDisabled?:boolean}} */
/** @returns {{agentName?:string, projectName?:string, initialMessage?:string, beta?:boolean, chatDisabled?:boolean, chatEnabled?:boolean}} */
function cfg() {
return (typeof window !== 'undefined' && window.RoundtableConfig) || {};
}
Expand Down Expand Up @@ -55,3 +55,13 @@ export function betaEnabled() {
export function chatDisabled() {
return flag(cfg().chatDisabled);
}

/**
* Whether the AI chat is enabled for this install (boot flag from Task 3's PHP
* plumbing). False (the default) hides all chat UI and gives the topics feed the
* full width of the container.
* @returns {boolean}
*/
export function chatEnabled() {
return flag(cfg().chatEnabled);
}
Loading
Loading