Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
04fe786
feat(chat): implement message regeneration and enhance datasource nav…
medazizktata25 Mar 3, 2026
b1b6e42
refactor: clean up code formatting and improve readability across mul…
medazizktata25 Mar 3, 2026
c00a83e
feat: enhance ToolErrorVisualizer with error details formatting and c…
medazizktata25 Mar 3, 2026
9112924
feat: enhance suggestion detection and rendering in Streamdown component
medazizktata25 Mar 3, 2026
84cb935
refactor(conversation): re-add load more buttonfunctionality in conve…
medazizktata25 Mar 3, 2026
8918e41
feat(code-block): enhance CodeBlock component with additional props f…
medazizktata25 Mar 4, 2026
c3529aa
feat(datasource): enhance DatasourceBadge and DatasourceSelector comp…
medazizktata25 Mar 4, 2026
65f3af0
feat: introduce ToolWithTaskDelimiter component and enhance message r…
medazizktata25 Mar 4, 2026
d75c0ea
feat(schema): enhance GetSchemaTool with improved error handling and …
medazizktata25 Mar 4, 2026
2cf2004
fix: simplify condition for checking todos in output object
medazizktata25 Mar 4, 2026
bc86652
feat: Add text highlighting and truncation utilities, implement messa…
medazizktata25 Mar 10, 2026
0a18f14
feat: Resolve merge conflicts, refine conversation list layout, and e…
medazizktata25 Mar 10, 2026
77c788e
feat: enhance tool titles to support ReactNode and display chart type…
medazizktata25 Mar 10, 2026
b017af4
refactor: Improve tool name and chart type extraction to support `Too…
medazizktata25 Mar 10, 2026
877df44
feat: Implement model selection and management UI
medazizktata25 Mar 11, 2026
6e34c5f
feat: Implement web search functionality with preferred search engine…
medazizktata25 Mar 11, 2026
6a41708
refactor: checks fixes
medazizktata25 Mar 11, 2026
940957a
refactor: simplify badge visibility conditions and reformat code in `…
medazizktata25 Mar 11, 2026
1cffb51
chore: agent-ui code file lint fix
medazizktata25 Mar 11, 2026
4b5a757
chore: merge origin/main into feat/tools-handling-and-ui
medazizktata25 Mar 13, 2026
86b285b
fix(ui): resolve searchEngine type mismatch in PromptInputInner
medazizktata25 Mar 13, 2026
2a1432c
feat: Enhance UI for message previews, schema visualization, and load…
medazizktata25 Mar 13, 2026
74a2d8c
fix(ui): adjust button class order in UserMessageBubble
medazizktata25 Mar 13, 2026
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
12 changes: 12 additions & 0 deletions apps/server/src/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type UIMessage,
} from '@qwery/agent-factory-sdk';
import { normalizeUIRole } from '@qwery/shared/message-role-utils';
import { MessageRole } from '@qwery/domain/entities';
import type { Repositories } from '@qwery/domain/repositories';
import { createRepositories } from '../lib/repositories';
import { getTelemetry } from '../lib/telemetry';
Expand Down Expand Up @@ -52,6 +53,17 @@ export function createChatRoutes() {
const model = body.model ?? getDefaultModel();

const repositories = await getRepositories();
if (body.trigger === 'regenerate-message') {
const conversation = await repositories.conversation.findBySlug(slug);
if (conversation) {
const convMessages =
await repositories.message.findByConversationId(conversation.id);
const lastMessage = convMessages.at(-1);
if (lastMessage && lastMessage.role === MessageRole.ASSISTANT) {
await repositories.message.delete(lastMessage.id);
}
}
}
const datasources = await resolveChatDatasources({
bodyDatasources: body.datasources,
messages,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const projectLayout = layout('routes/project/layout.tsx', [
const datasourceLayout = layout('routes/datasource/layout.tsx', [
route('ds/:slug', 'routes/datasource/index.tsx'),
route('ds/:slug/tables', 'routes/datasource/tables.tsx'),
route('ds/:slug/tables/:id', 'routes/datasource/table.tsx'),
route('ds/:slug/tables/:schema/:tableName', 'routes/datasource/table.tsx'),
route('ds/:slug/schema', 'routes/datasource/schema.tsx'),
route('ds/:slug/settings', 'routes/datasource/settings.tsx'),
]);
Expand Down
25 changes: 17 additions & 8 deletions apps/web/app/routes/datasource/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export async function loader(args: Route.LoaderArgs) {
export default function TablePage(props: Route.ComponentProps) {
const params = useParams();
const slug = params.slug as string;
const tableId = params.id as string;
const schemaParam = params.schema as string;
const tableNameParam = params.tableName as string;
const schema = schemaParam ? decodeURIComponent(schemaParam) : '';
const tableName = tableNameParam ? decodeURIComponent(tableNameParam) : '';
const { t } = useTranslation();
const { datasource } = props.loaderData;

Expand All @@ -43,18 +46,25 @@ export default function TablePage(props: Route.ComponentProps) {
});

const table = useMemo(() => {
if (!metadata?.tables) return null;
if (!metadata?.tables || !schema || !tableName) return null;
const tables = metadata.tables as Table[];
return tables.find((t) => t.id.toString() === tableId) || null;
}, [metadata, tableId]);
return (
tables.find(
(t) => (t.schema ?? 'main') === schema && t.name === tableName,
) ?? null
);
}, [metadata, schema, tableName]);

const columns = useMemo(() => {
if (!metadata?.columns || !table) return [];
const allColumns = metadata.columns as Column[];
return allColumns.filter(
(col) => col.table_id.toString() === tableId && col.table === table.name,
(col) =>
col.table_id === table.id &&
col.table === table.name &&
(col.schema ?? 'main') === (table.schema ?? 'main'),
);
}, [metadata, table, tableId]);
}, [metadata, table]);

const columnListItems: ColumnListItem[] = useMemo(() => {
return columns.map((col) => ({
Expand Down Expand Up @@ -89,7 +99,6 @@ export default function TablePage(props: Route.ComponentProps) {
);
}

const tableName = table.name;
const tablesPath = `/ds/${slug}/tables`;

return (
Expand All @@ -100,7 +109,7 @@ export default function TablePage(props: Route.ComponentProps) {
defaultValue: 'Tables',
})}
</Link>{' '}
<span className="text-muted-foreground">&gt;</span> {tableName}
<span className="text-muted-foreground">&gt;</span> {table.name}
</h1>
<Columns columns={columnListItems} />
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/routes/datasource/tables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export default function TablesPage(props: Route.ComponentProps) {
const handleTableClick = (table: TableListItem) => {
const tableData = filteredTables.find((t) => t.name === table.tableName);
if (!tableData) return;

const tablePath = `/ds/${slug}/tables/${tableData.id}`;
navigate(tablePath);
const schema = encodeURIComponent(tableData.schema ?? 'main');
const tableName = encodeURIComponent(tableData.name);
navigate(`/ds/${slug}/tables/${schema}/${tableName}`);
};

if (!datasource) {
Expand Down
96 changes: 86 additions & 10 deletions apps/web/app/routes/project/_components/agent-ui-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ export interface NoDatasourceDialogRef {
open: (text: string) => Promise<boolean>;
}

const ENABLED_MODELS_STORAGE_KEY = 'qwery-enabled-model-ids';

function loadEnabledModelIds(allModels: { value: string }[]): Set<string> {
if (typeof window === 'undefined')
return new Set(allModels.map((m) => m.value));
try {
const raw = localStorage.getItem(ENABLED_MODELS_STORAGE_KEY);
if (!raw) return new Set(allModels.map((m) => m.value));
const ids = JSON.parse(raw) as string[];
const valid = new Set(allModels.map((m) => m.value));
return new Set(ids.filter((id) => valid.has(id)));
} catch {
return new Set(allModels.map((m) => m.value));
}
}

function saveEnabledModelIds(ids: Set<string>) {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(ENABLED_MODELS_STORAGE_KEY, JSON.stringify([...ids]));
} catch {
/* ignore */
}
}

const NoDatasourceDialog = forwardRef<NoDatasourceDialogRef>(
function NoDatasourceDialog(_, ref) {
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -105,7 +130,14 @@ import { useAgentStatus, formatRelativeTime } from '@qwery/ui/ai';
import type { FeedbackPayload } from '@qwery/ui/ai';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { createDatasourceViewPath } from '~/config/project.navigation.config';
import {
createDatasourceViewPath,
createDatasourceTableViewPath,
} from '~/config/project.navigation.config';
import {
openDatasourceInNewTab,
openTableInNewTab,
} from '~/lib/utils/datasource-navigation';

type SendMessageFn = (
message: { text: string },
Expand Down Expand Up @@ -267,6 +299,27 @@ export const AgentUIWrapper = forwardRef<
| undefined
>(undefined);

const supportedModels = useMemo(
() => SUPPORTED_MODELS as { name: string; value: string }[],
[],
);
const [enabledModelIds, setEnabledModelIds] = useState<Set<string>>(() =>
loadEnabledModelIds(supportedModels),
);
const enabledModels = useMemo(
() => supportedModels.filter((m) => enabledModelIds.has(m.value)),
[supportedModels, enabledModelIds],
);

const handleModelsChange = useCallback(
(next: { name: string; value: string }[]) => {
const ids = new Set(next.map((m) => m.value));
setEnabledModelIds(ids);
saveEnabledModelIds(ids);
},
[],
);

// Track if we've already initialized datasource from cell to prevent overwriting user selections
const initializedCellDatasourceRef = useRef<string | null>(null);

Expand Down Expand Up @@ -734,14 +787,32 @@ export const AgentUIWrapper = forwardRef<
);

const _handleDatasourceNameClick = useCallback(
(idOrSlug: string, _name: string) => {
const ds =
datasourceItems.find((d) => d.id === idOrSlug) ??
datasourceItems.find((d) => d.slug === idOrSlug);
if (ds?.slug) {
const path = createDatasourceViewPath(ds.slug);
window.open(path, '_blank', 'noopener,noreferrer');
}
(idOrSlug: string, name: string) => {
openDatasourceInNewTab(
datasourceItems,
idOrSlug,
name,
createDatasourceViewPath,
);
},
[datasourceItems],
);

const _handleTableNameClick = useCallback(
(
datasourceIdOrSlug: string,
datasourceName: string,
schema: string,
tableName: string,
) => {
openTableInNewTab(
datasourceItems,
datasourceIdOrSlug,
datasourceName,
schema,
tableName,
createDatasourceTableViewPath,
);
},
[datasourceItems],
);
Expand All @@ -759,7 +830,9 @@ export const AgentUIWrapper = forwardRef<
<QweryAgentUI
transport={transport}
initialMessages={convertedInitialMessages}
models={SUPPORTED_MODELS as { name: string; value: string }[]}
models={enabledModels}
allModels={supportedModels}
onModelsChange={handleModelsChange}
usage={convertUsage(usage)}
emitFinish={handleEmitFinish}
datasources={datasourceItems}
Expand All @@ -775,6 +848,9 @@ export const AgentUIWrapper = forwardRef<
isLoading={isLoading}
conversationSlug={conversationSlug}
conversationTitle={conversationTitle ?? conversation?.title}
onDatasourceNameClick={_handleDatasourceNameClick}
onTableNameClick={_handleTableNameClick}
getDatasourceTooltip={_getDatasourceTooltip}
/>
<NoDatasourceDialog ref={noDatasourceDialogRef} />
</>
Expand Down
25 changes: 23 additions & 2 deletions apps/web/app/routes/project/conversation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,15 @@ export default function ConversationIndexPage() {
const isLoading = isLoadingConversations || isProjectLoading;
const [searchQuery, setSearchQuery] = useState('');
const [isEditMode, setIsEditMode] = useState(false);
const [loadMoreState, setLoadMoreState] = useState<{
hasMore: boolean;
onLoadMore: () => void;
isLoading: boolean;
} | null>(null);

return (
<div className="bg-background flex h-full w-full flex-col overflow-hidden">
<div className="flex h-full flex-col">
<div className="bg-background flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden">
<div className="flex h-full min-h-0 flex-1 flex-col">
<section className="flex shrink-0 flex-col gap-6 px-8 py-6 lg:px-16 lg:py-10">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">{t('chat:title')}</h1>
Expand Down Expand Up @@ -258,11 +263,27 @@ export default function ConversationIndexPage() {
onSearchQueryChange={setSearchQuery}
isEditMode={isEditMode}
onEditModeChange={setIsEditMode}
onLoadMoreStateChange={setLoadMoreState}
className="h-full"
/>
)}
</div>
</div>

{loadMoreState?.hasMore && (
<div className="bg-background flex shrink-0 items-center px-8 py-4 lg:px-16 lg:py-6">
<Button
variant="outline"
size="sm"
onClick={loadMoreState.onLoadMore}
disabled={loadMoreState.isLoading}
className="text-muted-foreground hover:text-foreground h-10 w-full border-2"
data-test="conversation-load-more"
>
{loadMoreState.isLoading ? 'Loading...' : 'Load more'}
</Button>
</div>
)}
</div>
</div>
);
Expand Down
33 changes: 33 additions & 0 deletions apps/web/components/account-dropdown-container.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,50 @@
'use client';

import { useEffect, useState } from 'react';
import { AccountDropdown } from '@qwery/accounts/account-dropdown';
import type { SearchEngine } from '@qwery/ui/ai';
import { isSearchEngine } from '@qwery/ui/ai';
import { WorkspaceModeEnum } from '@qwery/domain/enums';
import { useWorkspace } from '~/lib/context/workspace-context';
import { useSwitchWorkspaceMode } from '~/lib/hooks/use-workspace-mode';

import pathsConfig from '~/config/paths.config';

const PREFERRED_SEARCH_ENGINE_KEY = 'qwery-preferred-search-engine';

const paths = {
home: pathsConfig.app.home,
};

function getStoredSearchEngine(): SearchEngine {
if (typeof window === 'undefined') return 'google';
try {
const v = localStorage.getItem(PREFERRED_SEARCH_ENGINE_KEY);
return v && isSearchEngine(v) ? v : 'google';
} catch {
return 'google';
}
}

export function AccountDropdownContainer() {
const { workspace } = useWorkspace();
const { mutate: switchWorkspaceMode } = useSwitchWorkspaceMode();
const [preferredSearchEngine, setPreferredSearchEngine] =
useState<SearchEngine>(getStoredSearchEngine);

useEffect(() => {
try {
localStorage.setItem(PREFERRED_SEARCH_ENGINE_KEY, preferredSearchEngine);
} catch {
/* ignore */
}
}, [preferredSearchEngine]);

useEffect(() => {
const handler = () => setPreferredSearchEngine(getStoredSearchEngine());
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, []);

const handleWorkspaceModeChange = (mode: 'simple' | 'advanced') => {
const modeEnum =
Expand All @@ -34,6 +65,8 @@ export function AccountDropdownContainer() {
workspace.mode === WorkspaceModeEnum.ADVANCED ? 'advanced' : 'simple'
}
onWorkspaceModeChange={handleWorkspaceModeChange}
preferredSearchEngine={preferredSearchEngine}
onPreferredSearchEngineChange={setPreferredSearchEngine}
/>
);
}
7 changes: 5 additions & 2 deletions apps/web/components/workspace-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,12 @@ export function WorkspaceProvider(props: React.PropsWithChildren) {
const runtime = await workspaceService.execute();

const currentStored = getWorkspaceFromLocalStorage();
const nextUserId = currentStored.userId || uuidv4();
const nextWorkspaceId = currentStored.id || uuidv4();

const workspaceData: Workspace = {
id: currentStored.id || uuidv4(),
userId: currentStored.userId,
id: nextWorkspaceId,
userId: nextUserId,
username: currentStored.username,
organizationId: currentStored.organizationId,
projectId: currentStored.projectId,
Expand Down
12 changes: 12 additions & 0 deletions apps/web/config/project.navigation.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,15 @@ export function createDatasourcePath(slug: string, name: string) {
export function createDatasourceViewPath(slug: string) {
return createPath(pathsConfig.app.projectDatasourceView, slug);
}

/** Build path to a specific table: /ds/{slug}/tables/{schema}/{tableName} */
export function createDatasourceTableViewPath(
slug: string,
schema: string,
tableName: string,
) {
const base = createPath(pathsConfig.app.datasourceTables, slug);
const encodedSchema = encodeURIComponent(schema || 'main');
const encodedTableName = encodeURIComponent(tableName);
return `${base}/${encodedSchema}/${encodedTableName}`;
}
Loading
Loading