Skip to content

Adding chat config#7297

Merged
galvana merged 18 commits intomainfrom
slack-chat-provider
Feb 4, 2026
Merged

Adding chat config#7297
galvana merged 18 commits intomainfrom
slack-chat-provider

Conversation

@galvana
Copy link
Contributor

@galvana galvana commented Feb 3, 2026

Description Of Changes

This PR adds the foundation for Slack chat provider integration, allowing administrators to configure Slack workspace connections for sending notifications. The feature is gated behind the alphaDataProtectionAssessments flag.

The implementation includes:

  • Backend API routes for managing chat provider configurations (CRUD operations)
  • Slack OAuth 2.0 flow for workspace authorization
  • Service layer for business logic (single enabled provider at a time)
  • Endpoints for testing connections, listing channels, and sending messages

Note: The notification-integrations page UI has been excluded and will be included in a separate PR.

Code Changes

Backend (fidesplus)

  • Added ChatProviderConfigService for managing chat provider configurations with business logic
  • Added API routes at /api/v1/plus/chat/ for configuration CRUD, OAuth flow, and messaging
  • Added Pydantic schemas for chat provider configuration requests/responses
  • Added CHAT_PROVIDER_READ and CHAT_PROVIDER_UPDATE scopes to plus scope registry
  • Added SlackOAuth helper class for Slack OAuth 2.0 and API interactions
  • Added ChatProviderConfigNotFoundError exception type
  • Registered chat provider router in main.py

Frontend (fides)

  • Added chat provider configuration UI components
  • Added Slack integration card with conversation support
  • Added chat provider API slice and routes
  • Gated feature behind alphaDataProtectionAssessments flag

Database

  • Added ChatProviderConfig model with fields for provider type, credentials, OAuth tokens, and workspace info

Steps to Confirm

  1. Enable the alphaDataProtectionAssessments feature flag
  2. Navigate to the chat provider settings page
  3. Create a new Slack configuration with workspace URL and OAuth credentials
  4. Verify the OAuth flow redirects to Slack and back successfully
  5. Confirm only one chat provider can be enabled at a time
  6. Verify updating credentials clears the access token (requires re-authorization)

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

- Add chat provider configuration UI components
- Add Slack integration card with conversation support
- Add chat provider API slice and routes
- Add ChatProviderConfig model and database migration
- Gate feature behind alphaDataProtectionAssessments flag
- Exclude notification-integrations page (kept for separate PR)

Co-authored-by: Cursor <[email protected]>
@galvana galvana requested review from a team as code owners February 3, 2026 00:13
@galvana galvana requested review from jpople and thabofletcher and removed request for a team February 3, 2026 00:13
@vercel
Copy link
Contributor

vercel bot commented Feb 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fides-plus-nightly Ready Ready Preview, Comment Feb 3, 2026 11:05pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored Feb 3, 2026 11:05pm

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Greptile Overview

Greptile Summary

This PR adds a comprehensive Slack chat provider integration feature gated behind the alphaDataProtectionAssessments feature flag. The implementation includes:

Backend Changes:

  • New ChatProviderConfig model with encrypted credential storage (client_secret, access_token, signing_secret)
  • Database migration creating chat_provider_config table with single-row constraint
  • New config fields for privacy_assessments_channel in notification settings

Frontend Changes:

  • Complete Slack integration UI with OAuth connection flow
  • Questionnaire feature with conversation support and polling
  • RTK Query API slice with endpoints for settings, channels, messages, and questionnaires
  • Table view for managing chat provider configurations
  • Feature properly gated behind alpha flag with permissions checks

Key Issues:

  • PR size exceeds guidelines: 2,408 lines across 22 files (guideline: max 500 lines OR 15 files)
  • Critical: Polling intervals inappropriate for production: Questionnaire polling uses 3s/30s intervals instead of hours
  • SlackIntegrationCard.tsx is 699 lines (at the 700-line limit for components)
  • Minor: Migration uses sa.text("false") instead of "f" for boolean defaults
  • Minor: Missing invalidatesTags on sendChatMessage mutation

The feature is well-structured with proper encryption, permissions, and feature flagging, but needs adjustments before production deployment.

Confidence Score: 3/5

  • This PR is safe to merge for alpha/development use only, but requires critical polling interval fixes before any production deployment.
  • Score of 3 reflects: (1) critical production-readiness issue with polling intervals set to seconds instead of hours, (2) PR significantly exceeds size guidelines making thorough review difficult, (3) component at size limit suggesting future maintainability concerns. The implementation is otherwise well-structured with proper encryption, feature flagging, and permissions, making it acceptable for alpha testing but requiring fixes before broader deployment.
  • Pay special attention to SlackIntegrationCard.tsx (polling intervals must be updated before production) and the migration file (minor style issue with boolean defaults).

Important Files Changed

Filename Overview
src/fides/api/alembic/migrations/versions/xx_2026_01_14_1200_add_chat_provider_config.py Database migration adds chat_provider_config table with proper encryption for secrets. Minor style issue with boolean defaults.
src/fides/api/models/chat_provider_config.py SQLAlchemy model for chat provider config with proper encryption and single-row enforcement. Well-structured with helper methods.
clients/admin-ui/src/features/chat-provider/SlackIntegrationCard.tsx 699-line React component for Slack integration with OAuth flow, questionnaires. Has critical polling interval issue and size concerns.
clients/admin-ui/src/features/chat-provider/chatProvider.slice.ts RTK Query API slice with chat provider endpoints. Missing some invalidatesTags for optimal cache management.
clients/admin-ui/src/features/chat-provider/ChatProviderConfigurations.tsx Table view for chat provider configurations with proper permissions and delete confirmation modal.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 90 to 91
pollingInterval: hasActiveConversation ? 3000 : 30000,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polling intervals should be set in hours for production, not seconds. Current values (3s/30s) are only appropriate for development.

Suggested change
pollingInterval: hasActiveConversation ? 3000 : 30000,
});
pollingInterval: hasActiveConversation ? 3600000 : 14400000, // 1 hour / 4 hours

Context Used: Rule from dashboard - Polling intervals for async operations should be set in hours for production, not seconds or minutes... (source)

Comment on lines 1 to 699
import {
Button,
Card,
CUSTOM_TAG_COLOR,
Flex,
Input,
Select,
Space,
Tag,
Typography,
useChakraToast as useToast,
} from "fidesui";
import { useEffect, useState } from "react";

import { getErrorMessage, isErrorResult } from "~/features/common/helpers";
import { errorToastParams, successToastParams } from "~/features/common/toast";
import {
useGetConfigurationSettingsQuery,
usePatchConfigurationSettingsMutation,
} from "~/features/config-settings/config-settings.slice";

import {
ChatHistoryEntry,
Questionnaire,
useCreateQuestionnaireMutation,
useDeleteChatConnectionMutation,
useGetChatChannelsQuery,
useGetChatSettingsQuery,
useGetQuestionnairesQuery,
useSendChatMessageMutation,
useTestChatConnectionMutation,
useUpdateChatSettingsMutation,
} from "./chatProvider.slice";
import styles from "./SlackIntegrationCard.module.scss";

const { Text, Title, Paragraph } = Typography;

const SlackLogo = () => (
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z"
fill="#E01E5A"
/>
<path
d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z"
fill="#36C5F0"
/>
<path
d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.27 0a2.528 2.528 0 0 1-2.522 2.521 2.528 2.528 0 0 1-2.52-2.521V2.522A2.528 2.528 0 0 1 15.165 0a2.528 2.528 0 0 1 2.521 2.522v6.312z"
fill="#2EB67D"
/>
<path
d="M15.165 18.956a2.528 2.528 0 0 1 2.521 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.27a2.527 2.527 0 0 1-2.52-2.522 2.527 2.527 0 0 1 2.52-2.52h6.313A2.528 2.528 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.521h-6.313z"
fill="#ECB22E"
/>
</svg>
);

const SlackIntegrationCard = () => {
const toast = useToast();
const {
data: settings,
isLoading,
isError,
refetch,
} = useGetChatSettingsQuery();
const [updateSettings, { isLoading: isSaving }] =
useUpdateChatSettingsMutation();
const [testConnection, { isLoading: isTesting }] =
useTestChatConnectionMutation();
const [deleteConnection, { isLoading: isDeleting }] =
useDeleteChatConnectionMutation();
const [sendMessage, { isLoading: isSending }] = useSendChatMessageMutation();
const [createQuestionnaire, { isLoading: isCreatingQuestionnaire }] =
useCreateQuestionnaireMutation();

// Track if there's an active conversation for polling speed
const [hasActiveConversation, setHasActiveConversation] = useState(false);

// Questionnaire polling (provider-agnostic)
const { data: questionnairesData, refetch: refetchQuestionnaires } =
useGetQuestionnairesQuery(undefined, {
skip: !settings?.authorized,
pollingInterval: hasActiveConversation ? 3000 : 30000,
});

// Get the latest questionnaire
const latestQuestionnaire = questionnairesData?.questionnaires?.[0];

// Update active conversation state when data changes
useEffect(() => {
const isActive = latestQuestionnaire?.status === "in_progress";
setHasActiveConversation(isActive ?? false);
}, [latestQuestionnaire?.status]);

const { data: channelsData } = useGetChatChannelsQuery(undefined, {
skip: !settings?.authorized,
});

// Get and update global config for privacy assessments channel
const { data: appConfig } = useGetConfigurationSettingsQuery({
api_set: true,
});
const [patchConfigSettings] = usePatchConfigurationSettingsMutation();

const [selectedChannel, setSelectedChannel] = useState<string | undefined>(
undefined
);
const [testMessage, setTestMessage] = useState<string>(
"Hello from Fides! This is a test message."
);

// Credentials form state
const [clientId, setClientId] = useState<string>("");
const [clientSecret, setClientSecret] = useState<string>("");
const [signingSecret, setSigningSecret] = useState<string>("");

// Update selected channel when config settings load
useEffect(() => {
if (appConfig?.notifications?.privacy_assessments_channel) {
setSelectedChannel(appConfig.notifications.privacy_assessments_channel);
}
}, [appConfig?.notifications?.privacy_assessments_channel]);

// Handle OAuth callback results
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const chatSuccess = urlParams.get("chat_success");
const chatError = urlParams.get("chat_error");

if (chatSuccess === "true") {
toast(successToastParams("Slack connected successfully!"));
refetch();
window.history.replaceState({}, "", window.location.pathname);
} else if (chatError) {
const errorMessages: Record<string, string> = {
invalid_state:
"Authorization failed: Invalid state token. Please try again.",
not_configured: "Authorization failed: Chat provider not configured.",
token_failed: "Authorization failed: Could not obtain access token.",
no_token: "Authorization failed: No token received from Slack.",
};
toast(
errorToastParams(
errorMessages[chatError] ?? "Authorization failed. Please try again."
)
);
window.history.replaceState({}, "", window.location.pathname);
}
}, [refetch, toast]);

// Update form state when settings load (for editing existing credentials)
useEffect(() => {
if (settings?.client_id) {
setClientId(settings.client_id);
}
}, [settings?.client_id]);

const handleSaveCredentials = async () => {
if (!clientId.trim() || !clientSecret.trim()) {
toast(errorToastParams("Client ID and Client Secret are required."));
return;
}

const result = await updateSettings({
enabled: true,
provider_type: "slack",
client_id: clientId.trim(),
client_secret: clientSecret.trim(),
signing_secret: signingSecret.trim() || undefined,
});

if (isErrorResult(result)) {
toast(
errorToastParams(getErrorMessage(result.error, "Failed to save credentials."))
);
} else {
toast(successToastParams("Credentials saved. You can now connect to Slack."));
setClientSecret(""); // Clear secrets after save
setSigningSecret("");
refetch();
}
};

const handleConnect = () => {
window.location.href = "/api/v1/plus/chat/authorize";
};

const hasCredentials = Boolean(settings?.client_id);

const handleTestConnection = async () => {
const result = await testConnection();

if ("data" in result && result.data) {
if (result.data.success) {
toast(successToastParams(result.data.message));
} else {
toast(errorToastParams(result.data.message));
}
} else if (isErrorResult(result)) {
toast(errorToastParams(getErrorMessage(result.error, "Test failed.")));
}
};

const handleDisconnect = async () => {
const result = await deleteConnection();

if (isErrorResult(result)) {
toast(
errorToastParams(getErrorMessage(result.error, "Disconnect failed."))
);
} else {
toast(successToastParams("Slack disconnected successfully."));
refetch();
}
};

const handleChannelChange = async (value: string) => {
setSelectedChannel(value);

const result = await patchConfigSettings({
notifications: {
privacy_assessments_channel: value,
},
});

if (isErrorResult(result)) {
toast(errorToastParams(getErrorMessage(result.error, "Save failed.")));
} else {
toast(successToastParams("Channel updated."));
}
};

const handleSendMessage = async () => {
if (!selectedChannel || !testMessage.trim()) {
toast(errorToastParams("Please select a channel and enter a message."));
return;
}

const result = await sendMessage({
channel_id: selectedChannel,
message: testMessage,
});

if ("data" in result && result.data) {
if (result.data.success) {
toast(successToastParams(result.data.message));
} else {
toast(errorToastParams(result.data.message));
}
} else if (isErrorResult(result)) {
toast(
errorToastParams(getErrorMessage(result.error, "Failed to send message."))
);
}
};

// Questionnaire handler
const handleCreateQuestionnaire = async () => {
const result = await createQuestionnaire({
title: "Team Preferences Survey",
questions: [
"What's your favorite color?",
"What's your go-to comfort food?",
"Coffee or tea?",
"What's the best thing about working here?",
"What hobby would you pick up if you had unlimited time?",
"Does your organization sell personal information of California residents to third parties, or has it sold such information in the preceding 12 months?",
],
});

if ("data" in result && result.data) {
if (result.data.success) {
toast(successToastParams(result.data.message));
refetchQuestionnaires();
} else {
toast(errorToastParams(result.data.message));
}
} else if (isErrorResult(result)) {
toast(
errorToastParams(
getErrorMessage(result.error, "Failed to create questionnaire.")
)
);
}
};

const formatDateTime = (dateString: string) => {
return new Date(dateString).toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
};

const formatTime = (dateString: string) => {
return new Date(dateString).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
};

const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};

if (isLoading) {
return (
<Card className={styles.card}>
<Flex align="center" gap={12}>
<SlackLogo />
<Title level={5} className="m-0">
Slack
</Title>
</Flex>
<Paragraph className={styles.description}>Loading...</Paragraph>
</Card>
);
}

// Not connected state
if (!settings?.authorized || isError) {
return (
<Card className={styles.card} data-testid="slack-integration-card">
<div className={styles.header}>
<Flex align="center" gap={12}>
<SlackLogo />
<Title level={5} className="m-0">
Slack
</Title>
</Flex>
</div>

<Paragraph className={styles.description}>
Receive AI-powered privacy insights and alerts directly in your Slack
workspace.
</Paragraph>

<Flex align="center" gap={8} className="mb-4">
<Text strong>Status:</Text>
<Tag color={CUSTOM_TAG_COLOR.DEFAULT}>Not connected</Tag>
</Flex>

<Space direction="vertical" size={12} className="w-full mb-4">
<div>
<Text strong className="mb-1 block">
Slack app credentials
</Text>
<Text type="secondary" className="mb-3 block">
Create a Slack app at{" "}
<a
href="https://api.slack.com/apps"
target="_blank"
rel="noopener noreferrer"
>
api.slack.com/apps
</a>{" "}
and enter your app credentials below.
</Text>
</div>

<div>
<Text className="mb-1 block">Client ID</Text>
<Input
value={clientId}
onChange={(e) => setClientId(e.target.value)}
placeholder="Enter Slack app Client ID"
data-testid="slack-client-id-input"
/>
</div>

<div>
<Text className="mb-1 block">Client Secret</Text>
<Input.Password
value={clientSecret}
onChange={(e) => setClientSecret(e.target.value)}
placeholder={
hasCredentials
? "Enter new secret to update"
: "Enter Slack app Client Secret"
}
data-testid="slack-client-secret-input"
/>
</div>

<div>
<Text className="mb-1 block">
Signing Secret{" "}
<Text type="secondary">(optional, for webhooks)</Text>
</Text>
<Input.Password
value={signingSecret}
onChange={(e) => setSigningSecret(e.target.value)}
placeholder={
settings?.has_signing_secret
? "Enter new secret to update"
: "Enter Slack app Signing Secret"
}
data-testid="slack-signing-secret-input"
/>
</div>

<Flex gap={8}>
<Button
onClick={handleSaveCredentials}
loading={isSaving}
disabled={!clientId.trim() || !clientSecret.trim()}
data-testid="save-slack-credentials-btn"
>
Save credentials
</Button>

<Button
type="primary"
onClick={handleConnect}
disabled={!hasCredentials}
data-testid="connect-slack-btn"
>
Connect to Slack
</Button>
</Flex>

{!hasCredentials && (
<Text type="secondary">
Save your credentials first, then click &quot;Connect to
Slack&quot; to authorize.
</Text>
)}
</Space>
</Card>
);
}

// Connected state
return (
<Card className={styles.card} data-testid="slack-integration-card">
<div className={styles.header}>
<Flex align="center" gap={12}>
<SlackLogo />
<Title level={5} className="m-0">
Slack
</Title>
</Flex>
<Tag color={CUSTOM_TAG_COLOR.SUCCESS}>Connected</Tag>
</div>

<Space direction="vertical" size={4} className="mb-4">
{settings.workspace_name && (
<Text>
<Text strong>Workspace:</Text> {settings.workspace_name}
</Text>
)}
{settings.connected_by_email && (
<Text>
<Text strong>Connected by:</Text> {settings.connected_by_email}
</Text>
)}
{settings.created_at && (
<Text>
<Text strong>Connected on:</Text> {formatDate(settings.created_at)}
</Text>
)}
</Space>

<div className={styles.channelSection}>
<Text strong className="mb-2 block">
Notification settings
</Text>
<Flex align="center" gap={8}>
<Text>Channel for AI insights:</Text>
<Select
value={selectedChannel}
onChange={handleChannelChange}
placeholder="Select a channel"
className={styles.channelSelect}
options={channelsData?.channels?.map((channel) => ({
value: channel.id,
label: `#${channel.name}`,
}))}
loading={!channelsData}
data-testid="slack-channel-select"
/>
</Flex>
</div>

<div className={styles.testMessageSection}>
<Text strong className="mb-2 block">
Send test message
</Text>
<Flex gap={8}>
<Input
value={testMessage}
onChange={(e) => setTestMessage(e.target.value)}
placeholder="Enter a test message..."
className={styles.messageInput}
data-testid="slack-test-message-input"
/>
<Button
onClick={handleSendMessage}
loading={isSending}
disabled={!selectedChannel}
data-testid="send-slack-message-btn"
>
Send
</Button>
</Flex>
{!selectedChannel && (
<Text type="secondary" className="mt-1 block">
Select a channel first to send a test message.
</Text>
)}
</div>

<Flex gap={8} className="mt-4">
<Button
onClick={handleTestConnection}
loading={isTesting}
data-testid="test-slack-btn"
>
Test connection
</Button>
<Button
danger
onClick={handleDisconnect}
loading={isDeleting}
data-testid="disconnect-slack-btn"
>
Disconnect
</Button>
</Flex>

{/* Questionnaire Section */}
<div className={styles.qaSection}>
<Flex justify="space-between" align="center" className="mb-3">
<Text strong>Questionnaire</Text>
<Flex gap={8}>
<Button
onClick={() => refetchQuestionnaires()}
size="small"
data-testid="refresh-questionnaires-btn"
>
Refresh
</Button>
<Button
type="primary"
onClick={handleCreateQuestionnaire}
loading={isCreatingQuestionnaire}
disabled={!selectedChannel}
size="small"
data-testid="create-questionnaire-btn"
>
New questionnaire
</Button>
</Flex>
</Flex>

{!selectedChannel && (
<Text type="secondary" className="mb-3 block">
Select a channel above to start a questionnaire.
</Text>
)}

{latestQuestionnaire ? (
<QuestionnaireCard
questionnaire={latestQuestionnaire}
formatDateTime={formatDateTime}
formatTime={formatTime}
/>
) : (
<Text type="secondary">
No questionnaires yet. Click &quot;New questionnaire&quot; to create one.
</Text>
)}
</div>
</Card>
);
};

// Questionnaire Card with tabbed Q&A Summary and Full Conversation views
const QuestionnaireCard = ({
questionnaire,
formatDateTime,
formatTime,
}: {
questionnaire: Questionnaire;
formatDateTime: (dateString: string) => string;
formatTime: (dateString: string) => string;
}) => {
const [activeTab, setActiveTab] = useState<"qa" | "history">("qa");
const answeredCount = questionnaire.questions.filter(
(q) => q.answer !== null
).length;
const totalQuestions = questionnaire.questions.length;
const isCompleted = questionnaire.status === "completed";

return (
<Card size="small" className={styles.questionCard}>
<Flex justify="space-between" align="center" className="mb-2">
<Text strong>{questionnaire.title}</Text>
<Tag
color={
isCompleted ? CUSTOM_TAG_COLOR.SUCCESS : CUSTOM_TAG_COLOR.PROCESSING
}
>
{isCompleted
? "Completed"
: `${answeredCount}/${totalQuestions} answered`}
</Tag>
</Flex>
<Text type="secondary" className="block mb-3">
Started {formatDateTime(questionnaire.created_at)}
</Text>

{/* Simple tab buttons instead of Ant Tabs */}
<Flex gap={8} className="mb-3">
<Button
size="small"
type={activeTab === "qa" ? "primary" : "default"}
onClick={() => setActiveTab("qa")}
>
Q&A Summary
</Button>
<Button
size="small"
type={activeTab === "history" ? "primary" : "default"}
onClick={() => setActiveTab("history")}
>
Full Conversation
</Button>
</Flex>

{activeTab === "qa" && (
<div className={styles.answersContainer}>
{questionnaire.questions.map((q, idx) => (
<div key={idx} className={styles.qaItem}>
<Text strong className="block">
Q{idx + 1}: {q.displayed_as || q.question}
</Text>
{q.answer ? (
<Text className="block ml-4">
<Text type="secondary">A:</Text> {q.answer}
{(q.answered_by_email ?? q.answered_by_display_name) && (
<Text type="secondary">
{" "}
(by {q.answered_by_email ?? q.answered_by_display_name})
</Text>
)}
</Text>
) : idx === questionnaire.current_question_index && !isCompleted ? (
<Text type="secondary" italic className="block ml-4">
Waiting for answer...
</Text>
) : (
<Text type="secondary" className="block ml-4">
</Text>
)}
</div>
))}
</div>
)}

{activeTab === "history" && (
<div className={styles.chatHistoryContainer}>
{questionnaire.chat_history.map((entry, idx) => (
<div
key={idx}
className={entry.is_bot ? styles.botMessage : styles.userMessage}
>
<Flex justify="space-between" align="center" className="mb-1">
<Text strong>
{entry.is_bot
? "🤖 Astralis"
: `👤 ${entry.user_display_name ?? entry.user_email ?? "User"}`}
</Text>
<Text type="secondary">{formatTime(entry.timestamp)}</Text>
</Flex>
<Text className="block">{entry.text}</Text>
</div>
))}
</div>
)}
</Card>
);
};

export default SlackIntegrationCard;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component file is 699 lines, at the limit. Consider extracting:

  • SlackLogo component to separate file
  • QuestionnaireCard component to separate file (already defined at bottom)
  • Format functions (formatDateTime, formatTime, formatDate) to a utils/helpers file
  • Form state and handlers to a custom hook

Context Used: Rule from dashboard - Keep React component files under 700 lines and follow the practice of 1 component per file. Extract ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 54 to 55
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("single_row", sa.Boolean(), nullable=False, server_default=sa.text("true")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use server_default="f" format instead of sa.text("false") for consistency with project conventions.

Suggested change
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("single_row", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default="f"),
sa.Column("single_row", sa.Boolean(), nullable=False, server_default="t"),

Context Used: Rule from dashboard - Use server_default="f" for boolean columns in Alembic migrations instead of default=False or oth... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 157 to 163
sendChatMessage: build.mutation<SendMessageResponse, SendMessageRequest>({
query: (body) => ({
url: "plus/chat/send",
method: "POST",
body,
}),
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding invalidatesTags: ["Questionnaires"] to ensure questionnaire data refreshes after sending a message, as the message might trigger questionnaire updates.

Context Used: Rule from dashboard - When adding new RTK Query mutation endpoints, include invalidateTags to ensure related data refreshe... (source)

Copy link
Contributor

@lucanovera lucanovera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature was working correctly. I've pushed a few refactors to use Ant components, follow our FE best practices and to use the openapi generated type. Everything looks good now. Approved!

@galvana galvana changed the title Adding chat provider config Adding chat config Feb 3, 2026
@galvana galvana enabled auto-merge February 3, 2026 22:12
@galvana galvana added this pull request to the merge queue Feb 3, 2026
Merged via the queue into main with commit 5c70e20 Feb 4, 2026
59 of 61 checks passed
@galvana galvana deleted the slack-chat-provider branch February 4, 2026 00:02
@greptile-apps greptile-apps bot mentioned this pull request Feb 4, 2026
18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants