resolved conflicts

This commit is contained in:
Андрей Бабушкин 2025-05-27 17:53:13 +02:00
commit 48351965b7
41 changed files with 920 additions and 257 deletions

View file

@ -1,18 +1,13 @@
from chalicelib.utils import ch_client
from .events_pg import *
def __explode_properties(rows):
for i in range(len(rows)):
rows[i] = {**rows[i], **rows[i]["$properties"]}
rows[i].pop("$properties")
return rows
from chalicelib.utils.exp_ch_helper import explode_dproperties, add_timestamp
def get_customs_by_session_id(session_id, project_id):
with ch_client.ClickHouseClient() as cur:
rows = cur.execute(""" \
SELECT `$properties`,
properties,
created_at,
'CUSTOM' AS type
FROM product_analytics.events
@ -21,8 +16,10 @@ def get_customs_by_session_id(session_id, project_id):
AND `$event_name`!='INCIDENT'
ORDER BY created_at;""",
{"project_id": project_id, "session_id": session_id})
rows = __explode_properties(rows)
return helper.list_to_camel_case(rows)
rows = helper.list_to_camel_case(rows, ignore_keys=["properties"])
rows = explode_dproperties(rows)
rows = add_timestamp(rows)
return rows
def __merge_cells(rows, start, count, replacement):
@ -69,12 +66,13 @@ def get_by_session_id(session_id, project_id, group_clickrage=False, event_type:
parameters={"project_id": project_id, "session_id": session_id,
"select_events": select_events})
rows = cur.execute(query)
rows = __explode_properties(rows)
rows = explode_dproperties(rows)
if group_clickrage and 'CLICK' in select_events:
rows = __get_grouped_clickrage(rows=rows, session_id=session_id, project_id=project_id)
rows = helper.list_to_camel_case(rows)
rows = sorted(rows, key=lambda k: k["createdAt"])
rows = add_timestamp(rows)
return rows
@ -91,7 +89,7 @@ def get_incidents_by_session_id(session_id, project_id):
ORDER BY created_at;""",
parameters={"project_id": project_id, "session_id": session_id})
rows = cur.execute(query)
rows = __explode_properties(rows)
rows = explode_dproperties(rows)
rows = helper.list_to_camel_case(rows)
rows = sorted(rows, key=lambda k: k["createdAt"])
return rows

View file

@ -1,6 +1,6 @@
from chalicelib.utils import ch_client, helper
import datetime
from .issues_pg import get_all_types
from chalicelib.utils.exp_ch_helper import explode_dproperties, add_timestamp
def get(project_id, issue_id):
@ -21,7 +21,7 @@ def get(project_id, issue_id):
def get_by_session_id(session_id, project_id, issue_type=None):
with ch_client.ClickHouseClient() as cur:
query = cur.format(query=f"""\
SELECT *
SELECT created_at, `$properties`
FROM product_analytics.events
WHERE session_id = %(session_id)s
AND project_id= %(project_id)s
@ -29,8 +29,11 @@ def get_by_session_id(session_id, project_id, issue_type=None):
{"AND issue_type = %(type)s" if issue_type is not None else ""}
ORDER BY created_at;""",
parameters={"session_id": session_id, "project_id": project_id, "type": issue_type})
data = cur.execute(query)
return helper.list_to_camel_case(data)
rows = cur.execute(query)
rows = explode_dproperties(rows)
rows = helper.list_to_camel_case(rows)
rows = add_timestamp(rows)
return rows
# To reduce the number of issues in the replay;

View file

@ -1,13 +1,14 @@
import logging
import math
import re
import struct
from decimal import Decimal
from typing import Union, Any
import schemas
from chalicelib.utils import sql_helper as sh
from chalicelib.utils.TimeUTC import TimeUTC
from schemas import SearchEventOperator
import math
import struct
from decimal import Decimal
logger = logging.getLogger(__name__)
@ -233,3 +234,16 @@ def best_clickhouse_type(value):
return "Float64"
raise TypeError(f"Unsupported type: {type(value).__name__}")
def explode_dproperties(rows):
for i in range(len(rows)):
rows[i] = {**rows[i], **rows[i]["$properties"]}
rows[i].pop("$properties")
return rows
def add_timestamp(rows):
for row in rows:
row["timestamp"] = TimeUTC.datetime_to_timestamp(row["createdAt"])
return rows

View file

@ -15,11 +15,11 @@ def random_string(length=36):
return "".join(random.choices(string.hexdigits, k=length))
def list_to_camel_case(items: list[dict], flatten: bool = False) -> list[dict]:
def list_to_camel_case(items: list[dict], flatten: bool = False, ignore_keys=[]) -> list[dict]:
for i in range(len(items)):
if flatten:
items[i] = flatten_nested_dicts(items[i])
items[i] = dict_to_camel_case(items[i])
items[i] = dict_to_camel_case(items[i], ignore_keys=[])
return items

View file

@ -733,8 +733,8 @@ func (c *connectorImpl) InsertCustom(session *sessions.Session, msg *messages.Cu
if err != nil {
return fmt.Errorf("can't marshal custom event: %s", err)
}
var customPayload interface{}
if err := json.Unmarshal([]byte(msg.Payload), customPayload); err != nil {
customPayload := make(map[string]interface{})
if err := json.Unmarshal([]byte(msg.Payload), &customPayload); err != nil {
log.Printf("can't unmarshal custom event payload into object: %s", err)
customPayload = map[string]interface{}{
"payload": msg.Payload,

View file

@ -18,7 +18,7 @@ function KaiChat() {
const chatTitle = kaiStore.chatTitle;
const setTitle = kaiStore.setTitle;
const userId = userStore.account.id;
const userLetter = userStore.account.name[0].toUpperCase();
const userName = userStore.account.name;
const { activeSiteId } = projectsStore;
const [section, setSection] = React.useState<'intro' | 'chat'>('intro');
const [threadId, setThreadId] = React.useState<string | null>(null);
@ -44,7 +44,11 @@ function KaiChat() {
hideModal();
}}
/>,
{ right: true, width: 300 },
{
right: true,
width: 320,
className: 'bg-none flex items-center h-screen',
},
);
};
@ -93,44 +97,70 @@ function KaiChat() {
const newThread = await kaiService.createKaiChat(activeSiteId);
if (newThread) {
setThreadId(newThread.toString());
kaiStore.setTitle(null);
setSection('chat');
} else {
toast.error("Something wen't wrong. Please try again later.");
}
};
const onCancel = () => {
if (!threadId) return;
void kaiStore.cancelGeneration({
projectId: activeSiteId,
threadId,
});
};
return (
<div className="w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
<div
className="w-full mx-auto h-full"
style={{ maxWidth: PANEL_SIZES.maxWidth }}
>
<div
className={'w-full rounded-lg overflow-hidden border shadow relative'}
className={'w-full rounded-lg overflow-hidden bg-white relative h-full'}
>
<ChatHeader
chatTitle={chatTitle}
openChats={openChats}
goBack={goBack}
onCreate={onCreate}
/>
<div
className={
'w-full bg-active-blue flex flex-col items-center justify-center py-4 relative'
}
style={{
height: '70svh',
background:
'radial-gradient(50% 50% at 50% 50%, var(--color-glassWhite) 0%, var(--color-glassMint) 46%, var(--color-glassLavander) 100%)',
}}
>
{section === 'intro' ? (
<IntroSection onAsk={onCreate} />
) : (
<ChatLog
threadId={threadId}
projectId={activeSiteId}
userLetter={userLetter}
chatTitle={chatTitle}
initialMsg={initialMsg}
setInitialMsg={setInitialMsg}
/>
)}
</div>
{section === 'intro' ? (
<>
<div
className={
'flex flex-col items-center justify-center py-4 relative'
}
style={{
position: 'absolute',
top: '50%',
left: 0,
width: '100%',
transform: 'translateY(-50%)',
}}
>
<IntroSection
onCancel={onCancel}
onAsk={onCreate}
projectId={activeSiteId}
userName={userName}
/>
</div>
<div className={'text-disabled-text absolute bottom-4 left-0 right-0 text-center text-sm'}>
OpenReplay AI can make mistakes. Verify its outputs.
</div>
</>
) : (
<ChatLog
threadId={threadId}
projectId={activeSiteId}
chatTitle={chatTitle}
initialMsg={initialMsg}
setInitialMsg={setInitialMsg}
onCancel={onCancel}
/>
)}
</div>
</div>
);

View file

@ -36,6 +36,7 @@ export default class KaiService extends AiService {
supports_visualization: boolean;
chart: string;
chart_data: string;
sessions?: Record<string, any>[];
}[];
title: string;
}> => {
@ -123,4 +124,19 @@ export default class KaiService extends AiService {
const data = await r.json();
return data;
};
getPromptSuggestions = async (
projectId: string,
threadId?: string | null,
): Promise<string[]> => {
const endpoint = threadId
? `/kai/${projectId}/chats/${threadId}/prompt-suggestions`
: `/kai/${projectId}/prompt-suggestions`;
const r = await this.client.get(endpoint);
if (!r.ok) {
throw new Error('Failed to fetch prompt suggestions');
}
const data = await r.json();
return data;
};
}

View file

@ -3,6 +3,7 @@ import { BotChunk, ChatManager } from './SocketManager';
import { kaiService as aiService, kaiService } from 'App/services';
import { toast } from 'react-toastify';
import Widget from 'App/mstore/types/widget';
import Session, { ISession } from '@/types/session/session';
export interface Message {
text: string;
@ -15,6 +16,7 @@ export interface Message {
supports_visualization: boolean;
feedback: boolean | null;
duration: number;
sessions?: Session[];
}
export interface SentMessage
extends Omit<
@ -161,6 +163,9 @@ class KaiStore {
chart: m.chart,
supports_visualization: m.supports_visualization,
chart_data: m.chart_data,
sessions: m.sessions
? m.sessions.map((s) => new Session(s))
: undefined,
};
}),
);
@ -220,6 +225,9 @@ class KaiStore {
chart: '',
supports_visualization: msg.supports_visualization,
chart_data: '',
sessions: msg.sessions
? msg.sessions.map((s) => new Session(s))
: undefined,
};
this.bumpUsage();
this.addMessage(msgObj);
@ -268,7 +276,7 @@ class KaiStore {
deleting.push(this.lastKaiMessage.index);
}
this.deleteAtIndex(deleting);
this.setReplacing(false);
this.setReplacing(null);
}
this.addMessage({
text: message,
@ -309,7 +317,6 @@ class KaiStore {
cancelGeneration = async (settings: {
projectId: string;
userId: string;
threadId: string;
}) => {
try {

View file

@ -1,5 +1,6 @@
import io from 'socket.io-client';
import { toast } from 'react-toastify';
import { ISession } from '@/types/session/session';
export class ChatManager {
socket: ReturnType<typeof io>;
@ -77,9 +78,7 @@ export class ChatManager {
msgCallback,
titleCallback,
}: {
msgCallback: (
msg: StateEvent | BotChunk,
) => void;
msgCallback: (msg: StateEvent | BotChunk) => void;
titleCallback: (title: string) => void;
}) => {
this.socket.on('chunk', (msg: BotChunk) => {
@ -111,7 +110,8 @@ export interface BotChunk {
messageId: string;
duration: number;
supports_visualization: boolean;
type: 'chunk'
sessions?: ISession[];
type: 'chunk';
}
interface StateEvent {

View file

@ -1,56 +1,68 @@
import React from 'react';
import { Icon } from 'UI';
import { MessagesSquare, ArrowLeft } from 'lucide-react';
import { MessagesSquare, ArrowLeft, SquarePen } from 'lucide-react';
import { useTranslation } from 'react-i18next';
function ChatHeader({
openChats = () => {},
goBack,
chatTitle,
onCreate,
}: {
goBack?: () => void;
openChats?: () => void;
chatTitle: string | null;
onCreate: () => void;
}) {
const { t } = useTranslation();
//absolute top-0 left-0 right-0 z-10
return (
<div
className={
'px-4 py-2 flex items-center bg-white border-b border-b-gray-lighter'
}
>
<div className={'flex-1'}>
{goBack ? (
<div className="p-4 pb-0 w-full">
<div
className={'px-4 py-2 flex items-center bg-gray-lightest rounded-lg'}
>
<div className={'flex-1'}>
{goBack ? (
<div
className={
'w-fit flex items-center gap-2 font-semibold cursor-pointer hover:text-main'
}
onClick={goBack}
>
<ArrowLeft size={14} />
<div>{t('Back')}</div>
</div>
) : (
<div className="flex items-center gap-2">
<Icon name={'kai-mono'} size={18} />
<div className={'font-semibold text-xl'}>Kai</div>
</div>
)}
</div>
<div className={'flex items-center gap-2 mx-auto max-w-[80%]'}>
{chatTitle ? (
<div className="font-semibold text-xl whitespace-nowrap truncate">
{chatTitle}
</div>
) : null}
</div>
<div className={'flex-1 justify-end flex items-center gap-4'}>
{goBack ? (
<div
onClick={onCreate}
className="font-semibold w-fit cursor-pointer hover:text-main flex items-center gap-2"
>
<SquarePen size={14} />
<div>{t('New Chat')}</div>
</div>
) : null}
<div
className={
'w-fit flex items-center gap-2 font-semibold cursor-pointer'
}
onClick={goBack}
className="font-semibold w-fit cursor-pointer hover:text-main flex items-center gap-2"
onClick={openChats}
>
<ArrowLeft size={14} />
<div>{t('Back')}</div>
<MessagesSquare size={14} />
<div>{t('Chats')}</div>
</div>
) : null}
</div>
<div className={'flex items-center gap-2 mx-auto max-w-[80%]'}>
{chatTitle ? (
<div className="font-semibold text-xl whitespace-nowrap truncate">
{chatTitle}
</div>
) : (
<>
<Icon name={'kai_colored'} size={18} />
<div className={'font-semibold text-xl'}>Kai</div>
</>
)}
</div>
<div className={'flex-1 justify-end flex items-center gap-2'}>
<div
className="font-semibold w-fit cursor-pointer flex items-center gap-2"
onClick={openChats}
>
<MessagesSquare size={14} />
<div>{t('Chats')}</div>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Button, Input, Tooltip } from 'antd';
import { SendHorizonal, OctagonX } from 'lucide-react';
import { X, ArrowUp } from 'lucide-react';
import { kaiStore } from '../KaiStore';
import { observer } from 'mobx-react-lite';
import Usage from './Usage';
@ -8,11 +8,13 @@ import Usage from './Usage';
function ChatInput({
isLoading,
onSubmit,
threadId,
isArea,
onCancel,
}: {
isLoading?: boolean;
onSubmit: (str: string) => void;
threadId: string;
onCancel: () => void;
isArea?: boolean;
}) {
const inputRef = React.useRef<typeof Input>(null);
const usage = kaiStore.usage;
@ -28,8 +30,7 @@ function ChatInput({
return;
}
if (isProcessing) {
const settings = { projectId: '2325', userId: '0', threadId };
void kaiStore.cancelGeneration(settings);
onCancel();
} else {
if (inputValue.length > 0) {
onSubmit(inputValue);
@ -50,7 +51,34 @@ function ChatInput({
}, [inputValue]);
const isReplacing = kaiStore.replacing !== null;
const placeholder = limited
? `You've reached the daily limit for queries, come again tomorrow!`
: 'Ask anything about your product and users...';
if (isArea) {
return (
<div className="relative">
<Input.TextArea
rows={3}
className="!resize-none rounded-lg shadow"
onPressEnter={submit}
ref={inputRef}
placeholder={placeholder}
size={'large'}
disabled={limited}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<div className="absolute bottom-2 right-2">
<SendButton
isLoading={isLoading}
submit={submit}
limited={limited}
isProcessing={isProcessing}
/>
</div>
</div>
);
}
return (
<div className="relative">
<Input
@ -61,12 +89,9 @@ function ChatInput({
}
}}
ref={inputRef}
placeholder={
limited
? `You've reached the daily limit for queries, come again tomorrow!`
: 'Ask anything about your product and users...'
}
placeholder={placeholder}
size={'large'}
className="rounded-lg shadow"
disabled={limited}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
@ -76,31 +101,19 @@ function ChatInput({
<Tooltip title={'Cancel Editing'}>
<Button
onClick={cancelReplace}
icon={<OctagonX size={16} />}
type={'text'}
icon={<X size={16} />}
size={'small'}
shape={'circle'}
disabled={limited}
/>
</Tooltip>
) : null}
<Tooltip title={'Send message'}>
<Button
loading={isLoading}
onClick={submit}
disabled={limited}
icon={
isProcessing ? (
<OctagonX size={16} />
) : (
<SendHorizonal size={16} />
)
}
type={'text'}
size={'small'}
shape={'circle'}
/>
</Tooltip>
<SendButton
isLoading={isLoading}
submit={submit}
limited={limited}
isProcessing={isProcessing}
/>
</>
}
/>
@ -111,4 +124,42 @@ function ChatInput({
);
}
function SendButton({
isLoading,
submit,
limited,
isProcessing,
}: {
isLoading?: boolean;
submit: () => void;
limited: boolean;
isProcessing?: boolean;
}) {
return (
<Tooltip title={isProcessing ? 'Cancel processing' : 'Send message'}>
<Button
loading={isLoading}
onClick={submit}
disabled={limited}
icon={
isProcessing ? (
<X size={16} strokeWidth={2} />
) : (
<div className="bg-[#fff] text-main rounded-full">
<ArrowUp size={14} strokeWidth={2} />
</div>
)
}
type={'primary'}
size={'small'}
shape={isProcessing ? 'circle' : 'round'}
iconPosition={'end'}
className="font-semibold text-[#fff]"
>
{isProcessing ? null : 'Ask'}
</Button>
</Tooltip>
);
}
export default observer(ChatInput);

View file

@ -1,6 +1,7 @@
import React from 'react';
import ChatInput from './ChatInput';
import ChatMsg, { ChatNotice } from './ChatMsg';
import Ideas from './Ideas';
import { Loader } from 'UI';
import { kaiStore } from '../KaiStore';
import { observer } from 'mobx-react-lite';
@ -8,17 +9,17 @@ import { observer } from 'mobx-react-lite';
function ChatLog({
projectId,
threadId,
userLetter,
initialMsg,
chatTitle,
setInitialMsg,
onCancel,
}: {
projectId: string;
threadId: any;
userLetter: string;
initialMsg: string | null;
setInitialMsg: (msg: string | null) => void;
chatTitle: string | null;
onCancel: () => void;
}) {
const messages = kaiStore.messages;
const loading = kaiStore.loadingChat;
@ -50,11 +51,15 @@ function ChatLog({
});
}, [messages.length, processingStage]);
const lastHumanMsgInd: null | number = kaiStore.lastHumanMessage.index;
const lastKaiMessageInd: null | number = kaiStore.lastKaiMessage.index;
const lastHumanMsgInd: number | null = kaiStore.lastHumanMessage.index;
const showIdeas =
!processingStage && lastKaiMessageInd === messages.length - 1;
return (
<Loader loading={loading} className={'w-full h-full'}>
<div
ref={chatRef}
style={{ maxHeight: 'calc(100svh - 165px)' }}
className={
'overflow-y-auto relative flex flex-col items-center justify-between w-full h-full'
}
@ -63,7 +68,6 @@ function ChatLog({
{messages.map((msg, index) => (
<React.Fragment key={msg.messageId ?? index}>
<ChatMsg
userName={userLetter}
siteId={projectId}
message={msg}
chatTitle={chatTitle}
@ -81,9 +85,16 @@ function ChatLog({
duration={processingStage.duration}
/>
) : null}
{showIdeas ? (
<Ideas
onClick={(query) => onSubmit(query)}
projectId={projectId}
threadId={threadId}
/>
) : null}
</div>
<div className={'sticky bottom-0 pt-6 w-2/3'}>
<ChatInput onSubmit={onSubmit} threadId={threadId} />
<div className={'sticky bottom-0 pt-6 w-2/3 z-50'}>
<ChatInput onCancel={onCancel} onSubmit={onSubmit} />
</div>
</div>
</Loader>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Icon, CopyButton } from 'UI';
import { CopyButton } from 'UI';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import Markdown from 'react-markdown';
@ -8,7 +8,7 @@ import {
Loader,
ThumbsUp,
ThumbsDown,
ListRestart,
SquarePen,
FileDown,
Clock,
ChartLine,
@ -20,6 +20,7 @@ import { durationFormatted } from 'App/date';
import WidgetChart from '@/components/Dashboard/components/WidgetChart';
import Widget from 'App/mstore/types/widget';
import { useTranslation } from 'react-i18next';
import SessionItem from 'Shared/SessionItem';
function ChatMsg({
userName,
@ -168,28 +169,12 @@ function ChatMsg({
}, [metricData, chart_data]);
return (
<div className={cn('flex gap-2', isUser ? 'flex-row-reverse' : 'flex-row')}>
{isUser ? (
<div
className={
'rounded-full bg-main text-white min-w-8 min-h-8 max-h-8 max-w-8 flex items-center justify-center sticky top-0 mt-2 shadow'
}
>
<span className={'font-semibold'}>{userName}</span>
</div>
) : (
<div
className={
'rounded-full bg-gray-lightest shadow min-w-8 min-h-8 max-h-8 max-w-8 flex items-center justify-center sticky top-0 mt-2'
}
>
<Icon name={'kai_colored'} size={18} />
</div>
)}
<div className={'mt-1 flex flex-col'}>
<div className={'mt-1 flex flex-col group/actions'}>
<div
className={cn(
'markdown-body',
isEditing ? 'border-l border-l-main pl-2' : '',
isUser ? 'bg-gray-lighter px-4 rounded-full' : '',
isEditing ? '!bg-active-blue' : '',
)}
data-openreplay-obscured
ref={bodyRef}
@ -204,32 +189,42 @@ function ChatMsg({
<WidgetChart metric={metric} isPreview height={360} />
</div>
) : null}
{message.sessions ? (
<div className="flex flex-col">
{message.sessions.map((session) => (
<div className="shadow border rounded-xl overflow-hidden mb-2">
<SessionItem key={session.sessionId} session={session} slim />
</div>
))}
</div>
) : null}
{isUser ? (
<>
<div
onClick={onEdit}
className={cn(
'ml-auto flex items-center gap-2 px-2',
'rounded-lg border border-gray-medium text-sm cursor-pointer',
'hover:border-main hover:text-main w-fit',
canEdit && !isEditing ? '' : 'hidden',
)}
>
<ListRestart size={16} />
<div>{t('Edit')}</div>
</div>
<div className="invisible group-hover/actions:visible mt-2">
<Tooltip title={t('Edit')}>
<div
onClick={onEdit}
className={cn(
'ml-auto flex items-center gap-2 px-2',
'rounded-lg cursor-pointer',
'hover:text-main w-fit',
canEdit && !isEditing ? '' : 'hidden',
)}
>
<SquarePen size={16} />
</div>
</Tooltip>
<div
onClick={onCancelEdit}
className={cn(
'ml-auto flex items-center gap-2 px-2',
'rounded-lg border border-gray-medium text-sm cursor-pointer',
'rounded-lg border border-gray-medium text-xs cursor-pointer',
'hover:border-main hover:text-main w-fit',
isEditing ? '' : 'hidden',
)}
>
<div>{t('Cancel')}</div>
</div>
</>
</div>
) : (
<div className={'flex items-center gap-2'}>
{duration ? <MsgDuration duration={duration} /> : null}

View file

@ -44,10 +44,13 @@ function ChatsModal({
refetch();
};
return (
<div className={'h-screen w-full flex flex-col gap-2 p-4'}>
<div
className={'flex flex-col gap-2 p-4 mr-1 rounded-lg bg-white my-auto'}
style={{ height: '95svh', width: 310 }}
>
<div className={'flex items-center font-semibold text-lg gap-2'}>
<MessagesSquare size={16} />
<span>{t('Chats')}</span>
<span>{t('Previous Chats')}</span>
</div>
{usage.percent > 80 ? (
<div className="text-red text-sm">

View file

@ -1,30 +1,72 @@
import React from 'react';
import { Lightbulb, MoveRight } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { kaiService } from 'App/services';
import { useTranslation } from 'react-i18next';
function Ideas({ onClick }: { onClick: (query: string) => void }) {
function Ideas({
onClick,
projectId,
threadId = null,
}: {
onClick: (query: string) => void;
projectId: string;
threadId?: string | null;
}) {
const { t } = useTranslation();
const { data: suggestedPromptIdeas = [], isPending } = useQuery({
queryKey: ['kai', projectId, 'chats', threadId, 'prompt-suggestions'],
queryFn: () => kaiService.getPromptSuggestions(projectId, threadId),
staleTime: 1000 * 60,
});
const ideas = React.useMemo(() => {
const defaultPromptIdeas = [
'Top user journeys',
'Where do users drop off',
'Failed network requests today',
];
const result = suggestedPromptIdeas;
const targetSize = 3;
while (result.length < targetSize && defaultPromptIdeas.length) {
result.push(defaultPromptIdeas.pop());
}
return result;
}, [suggestedPromptIdeas.length]);
return (
<>
<div>
<div className={'flex items-center gap-2 mb-1 text-gray-dark'}>
<Lightbulb size={16} />
<b>Ideas:</b>
<b>Suggested Ideas:</b>
</div>
<IdeaItem onClick={onClick} title={'Top user journeys'} />
<IdeaItem onClick={onClick} title={'Where do users drop off'} />
<IdeaItem onClick={onClick} title={'Failed network requests today'} />
</>
{isPending ? (
<div className="animate-pulse text-disabled-text">
{t('Generating ideas')}...
</div>
) : (
<div className="flex gap-2 flex-wrap">
{ideas.map((title) => (
<IdeaItem key={title} onClick={onClick} title={title} />
))}
</div>
)}
</div>
);
}
function IdeaItem({ title, onClick }: { title: string, onClick: (query: string) => void }) {
function IdeaItem({
title,
onClick,
}: {
title: string;
onClick: (query: string) => void;
}) {
return (
<div
onClick={() => onClick(title)}
className={
'flex items-center gap-2 cursor-pointer text-gray-dark hover:text-black'
'cursor-pointer text-gray-dark hover:text-black rounded-full px-4 py-2 shadow border'
}
>
<MoveRight size={16} />
<span>{title}</span>
{title}
</div>
);
}

View file

@ -2,23 +2,33 @@ import React from 'react';
import ChatInput from './ChatInput';
import Ideas from './Ideas';
function IntroSection({ onAsk }: { onAsk: (query: string) => void }) {
function IntroSection({
onAsk,
onCancel,
userName,
projectId,
}: {
onAsk: (query: string) => void;
projectId: string;
onCancel: () => void;
userName: string;
}) {
const isLoading = false;
return (
<>
<div className={'text-disabled-text text-xl absolute top-4'}>
Kai is your AI assistant, delivering smart insights in response to your
queries.
</div>
<div className={'relative w-2/3'} style={{ height: 44 }}>
{/*<GradientBorderInput placeholder={'Ask anything about your product and users...'} onButtonClick={() => null} />*/}
<ChatInput isLoading={isLoading} onSubmit={onAsk} />
<div className={'absolute top-full flex flex-col gap-2 mt-4'}>
<Ideas onClick={(query) => onAsk(query)} />
<div className={'relative w-2/3 flex flex-col gap-4'}>
<div className="font-semibold text-lg">
Hey {userName}, how can I help you?
</div>
<ChatInput
onCancel={onCancel}
isLoading={isLoading}
onSubmit={onAsk}
isArea
/>
<div className={'absolute top-full flex flex-col gap-2 mt-4'}>
<Ideas onClick={(query) => onAsk(query)} projectId={projectId} />
</div>
</div>
<div className={'text-disabled-text absolute bottom-4'}>
OpenReplay AI can make mistakes. Verify its outputs.
</div>
</>
);

View file

@ -1,6 +1,7 @@
// @ts-nocheck
import React, { Component, createContext } from 'react';
import Modal from './Modal';
import { className } from '@medv/finder';
const ModalContext = createContext({
component: null,
@ -29,6 +30,7 @@ export class ModalProvider extends Component {
this.setState({
component,
props,
className: props.className || undefined,
});
document.addEventListener('keydown', this.handleKeyDown);
document.querySelector('body').style.overflow = 'hidden';

View file

@ -71,6 +71,7 @@ interface Props {
bookmarked?: boolean;
toggleFavorite?: (sessionId: string) => void;
query?: string;
slim?: boolean;
}
const PREFETCH_STATE = {
@ -99,6 +100,7 @@ function SessionItem(props: RouteComponentProps & Props) {
isDisabled,
live: propsLive,
isAdd,
slim,
} = props;
const {
@ -261,7 +263,7 @@ function SessionItem(props: RouteComponentProps & Props) {
}
>
<div
className={cn(stl.sessionItem, 'flex flex-col p-4')}
className={cn(stl.sessionItem, 'flex flex-col', slim ? 'px-4 py-2' : 'p-4')}
id="session-item"
onClick={(e) => e.stopPropagation()}
onMouseEnter={handleHover}
@ -343,7 +345,7 @@ function SessionItem(props: RouteComponentProps & Props) {
: 'Event'}
</span>
</div>
<Icon name="circle-fill" size={3} className="mx-4" />
<Icon name="circle-fill" size={3} className="mx-2" />
</>
)}
<div>
@ -373,7 +375,7 @@ function SessionItem(props: RouteComponentProps & Props) {
</span>
)}
{userOs && userBrowser && (
<Icon name="circle-fill" size={3} className="mx-4" />
<Icon name="circle-fill" size={3} className="mx-2" />
)}
{userOs && (
<span
@ -387,7 +389,7 @@ function SessionItem(props: RouteComponentProps & Props) {
</span>
)}
{userOs && (
<Icon name="circle-fill" size={3} className="mx-4" />
<Icon name="circle-fill" size={3} className="mx-2" />
)}
<span className="capitalize" style={{ maxWidth: '70px' }}>
<TextEllipsis

View file

@ -354,6 +354,7 @@ export { default as Integrations_teams } from './integrations_teams';
export { default as Integrations_vuejs } from './integrations_vuejs';
export { default as Integrations_zustand } from './integrations_zustand';
export { default as Journal_code } from './journal_code';
export { default as Kai_mono } from './kai_mono';
export { default as Kai } from './kai';
export { default as Kai_colored } from './kai_colored';
export { default as Key } from './key';

View file

@ -0,0 +1,18 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Kai_mono(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 20 20" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><g fill="#394DFE"><path d="M13.125 6.458a.625.625 0 1 0-1.25 0c0 2.022-.447 3.335-1.264 4.153-.818.817-2.131 1.264-4.153 1.264a.625.625 0 1 0 0 1.25c2.022 0 3.335.447 4.152 1.264.818.818 1.265 2.131 1.265 4.153a.625.625 0 1 0 1.25 0c0-2.022.447-3.335 1.264-4.153.818-.817 2.131-1.264 4.153-1.264a.625.625 0 1 0 0-1.25c-2.022 0-3.335-.447-4.153-1.264-.817-.818-1.264-2.131-1.264-4.153ZM6.042 1.458a.625.625 0 1 0-1.25 0c0 1.298-.288 2.09-.766 2.568-.478.478-1.27.766-2.568.766a.625.625 0 1 0 0 1.25c1.298 0 2.09.287 2.568.765.478.478.766 1.27.766 2.568a.625.625 0 0 0 1.25 0c0-1.298.287-2.09.765-2.568.478-.478 1.27-.765 2.568-.765a.625.625 0 0 0 0-1.25c-1.298 0-2.09-.288-2.568-.766-.478-.478-.765-1.27-.765-2.568Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h20v20H0z"/></clipPath></defs></svg>
);
}
export default Kai_mono;

View file

@ -356,6 +356,7 @@ import {
Integrations_vuejs,
Integrations_zustand,
Journal_code,
Kai_mono,
Kai,
Kai_colored,
Key,
@ -1565,6 +1566,9 @@ const SVG = (props: Props) => {
// case 'journal-code':
case 'journal-code': return <Journal_code width={ width } height={ height } fill={ fill } />;
// case 'kai-mono':
case 'kai-mono': return <Kai_mono width={ width } height={ height } fill={ fill } />;
case 'kai': return <Kai width={ width } height={ height } fill={ fill } />;

View file

@ -0,0 +1,11 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8_72)">
<path d="M13.1249 6.45831C13.1249 6.11313 12.8451 5.83331 12.4999 5.83331C12.1547 5.83331 11.8749 6.11313 11.8749 6.45831C11.8749 8.47982 11.4283 9.7927 10.6105 10.6106C9.79264 11.4284 8.47976 11.875 6.45825 11.875C6.11307 11.875 5.83325 12.1548 5.83325 12.5C5.83325 12.8452 6.11307 13.125 6.45825 13.125C8.47976 13.125 9.79264 13.5716 10.6105 14.3894C11.4283 15.2073 11.8749 16.5201 11.8749 18.5416C11.8749 18.8868 12.1547 19.1666 12.4999 19.1666C12.8451 19.1666 13.1249 18.8868 13.1249 18.5416C13.1249 16.5201 13.5715 15.2073 14.3893 14.3894C15.2072 13.5716 16.5201 13.125 18.5416 13.125C18.8868 13.125 19.1666 12.8452 19.1666 12.5C19.1666 12.1548 18.8868 11.875 18.5416 11.875C16.5201 11.875 15.2072 11.4284 14.3893 10.6106C13.5715 9.7927 13.1249 8.47982 13.1249 6.45831Z" fill="#394DFE"/>
<path d="M6.04158 1.45831C6.04158 1.11313 5.76176 0.833313 5.41658 0.833313C5.07141 0.833313 4.79158 1.11313 4.79158 1.45831C4.79158 2.75644 4.50416 3.54849 4.02629 4.02635C3.54843 4.50422 2.75638 4.79165 1.45825 4.79165C1.11307 4.79165 0.833252 5.07147 0.833252 5.41665C0.833252 5.76182 1.11307 6.04165 1.45825 6.04165C2.75638 6.04165 3.54843 6.32907 4.02629 6.80694C4.50416 7.2848 4.79158 8.07685 4.79158 9.37498C4.79158 9.72016 5.07141 9.99998 5.41658 9.99998C5.76176 9.99998 6.04158 9.72016 6.04158 9.37498C6.04158 8.07685 6.32901 7.2848 6.80688 6.80694C7.28474 6.32907 8.07679 6.04165 9.37492 6.04165C9.7201 6.04165 9.99992 5.76182 9.99992 5.41665C9.99992 5.07147 9.7201 4.79165 9.37492 4.79165C8.07679 4.79165 7.28474 4.50422 6.80688 4.02635C6.32901 3.54849 6.04158 2.75644 6.04158 1.45831Z" fill="#394DFE"/>
</g>
<defs>
<clipPath id="clip0_8_72">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,3 +1,4 @@
{{- if .Values.configOverride.serverConfig }}
---
apiVersion: v1
kind: ConfigMap
@ -8,6 +9,7 @@ data:
{{ $filename }}: |-
{{ $content | indent 4 }}
{{- end }}
{{- end }}
---
apiVersion: v1
kind: ConfigMap

View file

@ -87,27 +87,26 @@ storageSize: 100Gi
configOverride:
serverConfig:
zz-server-override.xml: |-
# <clickhouse>
# <logger>
# <level>information</level>
# <console>true</console>
# <log remove="remove"></log>
# <errorlog remove="remove"></errorlog>
# </logger>
# <listen_host>0.0.0.0</listen_host>
# <keep_alive_timeout>100</keep_alive_timeout>
# <concurrent_threads_soft_limit_num>64</concurrent_threads_soft_limit_num>
# <concurrent_threads_soft_limit_ratio_to_cores>2</concurrent_threads_soft_limit_ratio_to_cores>
# <concurrent_threads_scheduler>fair_round_robin</concurrent_threads_scheduler>
# <max_server_memory_usage>102400000000</max_server_memory_usage>
# <max_thread_pool_size>10000</max_thread_pool_size>
# <max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
# <uncompressed_cache_size remove="remove"></uncompressed_cache_size>
# <mmap_cache_size>26214</mmap_cache_size>
# </clickhouse>
<clickhouse>
<logger>
<level>information</level>
<console>true</console>
<log remove="remove"></log>
<errorlog remove="remove"></errorlog>
</logger>
<listen_host>0.0.0.0</listen_host>
<keep_alive_timeout>100</keep_alive_timeout>
</clickhouse>
# another-config.xml: |-
# <clickhouse>
# <another_setting>value</another_setting>
# <concurrent_threads_soft_limit_num>64</concurrent_threads_soft_limit_num>
# <concurrent_threads_soft_limit_ratio_to_cores>2</concurrent_threads_soft_limit_ratio_to_cores>
# <concurrent_threads_scheduler>fair_round_robin</concurrent_threads_scheduler>
# <max_server_memory_usage>102400000000</max_server_memory_usage>
# <max_thread_pool_size>10000</max_thread_pool_size>
# <max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
# <uncompressed_cache_size remove="remove"></uncompressed_cache_size>
# <mmap_cache_size>26214</mmap_cache_size>
# </clickhouse>
userConfig:
zz-user-override.xml: |-

View file

@ -37,3 +37,7 @@ dependencies:
repository: file://charts/connector
version: 0.1.1
condition: connector.enabled
- name: assist-api
repository: file://charts/assist-api
version: 0.1.1
condition: assist-api.enabled

View file

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View file

@ -0,0 +1,24 @@
apiVersion: v2
name: assist-api
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rassist-apiing
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.22.0"

View file

@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "assist-api.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "assist-api.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "assist-api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "assist-api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View file

@ -0,0 +1,65 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "assist-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "assist-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "assist-api.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "assist-api.labels" -}}
helm.sh/chart: {{ include "assist-api.chart" . }}
{{ include "assist-api.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- if .Values.global.appLabels }}
{{- .Values.global.appLabels | toYaml | nindent 0}}
{{- end}}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "assist-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "assist-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "assist-api.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "assist-api.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,101 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "assist-api.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "assist-api.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "assist-api.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "assist-api.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "assist-api.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
shareProcessNamespace: true
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.global.enterpriseEditionLicense }}
image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}-ee"
{{- else }}
image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.healthCheck}}
{{- .Values.healthCheck | toYaml | nindent 10}}
{{- end}}
env:
- name: LICENSE_KEY
value: '{{ .Values.global.enterpriseEditionLicense }}'
- name: ASSIST_KEY
value: {{ .Values.global.assistKey }}
- name: pg_password
{{- if .Values.global.postgresql.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.global.postgresql.existingSecret }}
key: postgresql-postgres-password
{{- else }}
value: '{{ .Values.global.postgresql.postgresqlPassword }}'
{{- end}}
- name: POSTGRES_STRING
value: 'postgres://{{ .Values.global.postgresql.postgresqlUser }}:$(pg_password)@{{ .Values.global.postgresql.postgresqlHost }}:{{ .Values.global.postgresql.postgresqlPort }}/{{ .Values.global.postgresql.postgresqlDatabase }}'
- name: REDIS_CACHE_ENABLED
value: {{ if .Values.global.enterpriseEditionLicense }}"true"{{ else }}"false"{{ end }}
{{- include "openreplay.env.redis_string" .Values.global.redis | nindent 12 }}
{{- range $key, $val := .Values.global.env }}
- name: {{ $key }}
value: '{{ $val }}'
{{- end }}
{{- range $key, $val := .Values.env }}
- name: {{ $key }}
value: '{{ $val }}'
{{- end}}
ports:
{{- range $key, $val := .Values.service.ports }}
- name: {{ $key }}
containerPort: {{ $val }}
protocol: TCP
{{- end }}
volumeMounts:
{{- include "openreplay.volume.redis_ca_certificate.mount" .Values.global.redis | nindent 12 }}
{{- with .Values.persistence.mounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
{{- with .Values.persistence.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- include "openreplay.volume.redis_ca_certificate" .Values.global.redis | nindent 8 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -0,0 +1,33 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "assist-api.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "assist-api.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "assist-api.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "assist-api.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "assist-api.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
{{- range $key, $val := .Values.service.ports }}
- port: {{ $val }}
targetPort: {{ $key }}
protocol: TCP
name: {{ $key }}
{{- end}}
selector:
{{- include "assist-api.selectorLabels" . | nindent 4 }}

View file

@ -0,0 +1,18 @@
{{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.serviceMonitor.enabled ) }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "assist-api.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "assist-api.labels" . | nindent 4 }}
{{- if .Values.serviceMonitor.additionalLabels }}
{{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }}
{{- end }}
spec:
endpoints:
{{- .Values.serviceMonitor.scrapeConfigs | toYaml | nindent 4 }}
selector:
matchLabels:
{{- include "assist-api.selectorLabels" . | nindent 6 }}
{{- end }}

View file

@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "assist-api.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "assist-api.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "assist-api.fullname" . }}-test-connection"
labels:
{{- include "assist-api.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "assist-api.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View file

@ -0,0 +1,119 @@
# Default values for openreplay.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: "{{ .Values.global.openReplayContainerRegistry }}/assist-api"
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: "assist-api"
fullnameOverride: "assist-api-openreplay"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
securityContext:
runAsUser: 1001
runAsGroup: 1001
podSecurityContext:
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
fsGroupChangePolicy: "OnRootMismatch"
# podSecurityContext: {}
# fsGroup: 2000
# securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
#service:
# type: ClusterIP
# port: 9000
serviceMonitor:
enabled: false
additionalLabels:
release: observability
scrapeConfigs:
- port: metrics
honorLabels: true
interval: 15s
path: /metrics
scheme: http
scrapeTimeout: 10s
service:
type: ClusterIP
ports:
http: 8080
metrics: 8888
ingress:
enabled: false
className: "{{ .Values.global.ingress.controller.ingressClassResource.name }}"
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
tls:
secretName: openreplay-ssl
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
env:
CLEAR_SOCKET_TIME: 720
nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -49,32 +49,18 @@ spec:
value: {{ .Values.global.assistKey }}
- name: AWS_DEFAULT_REGION
value: "{{ .Values.global.s3.region }}"
- name: S3_HOST
{{- if contains "minio" .Values.global.s3.endpoint }}
value: '{{ ternary "https" "http" .Values.global.ORSecureAccess}}://{{ .Values.global.domainName }}:{{ ternary .Values.global.ingress.controller.service.ports.https .Values.global.ingress.controller.service.ports.http .Values.global.ORSecureAccess }}'
{{- else}}
value: '{{ .Values.global.s3.endpoint }}'
{{- end}}
- name: S3_KEY
{{- if .Values.global.s3.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.global.s3.existingSecret }}
key: access-key
{{- else }}
value: {{ .Values.global.s3.accessKey }}
{{- end }}
- name: S3_SECRET
{{- if .Values.global.s3.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.global.s3.existingSecret }}
key: secret-key
{{- else }}
value: {{ .Values.global.s3.secretKey }}
{{- end }}
- name: REDIS_URL
value: {{ .Values.global.redis.redisHost }}
value: {{ default .Values.global.redis.redisHost .Values.redisHost }}
{{- if .Values.global.enterpriseEditionLicense }}
- name: COMPRESSION
value: "true"
- name: port
value: "9000"
- name: CACHE_REFRESH_INTERVAL_SECONDS
value: '5'
- name: debug
value: '0'
{{- end}}
{{- range $key, $val := .Values.global.env }}
- name: {{ $key }}
value: '{{ $val }}'

View file

@ -10,25 +10,6 @@ metadata:
{{- include "assist.labels" . | nindent 4 }}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
nginx.ingress.kubernetes.io/configuration-snippet: |
#set $sticky_used "no";
#if ($sessionid != "") {
# set $sticky_used "yes";
#}
#add_header X-Debug-Session-ID $sessionid;
#add_header X-Debug-Session-Type "wss";
#add_header X-Sticky-Session-Used $sticky_used;
#add_header X-Upstream-Server $upstream_addr;
proxy_hide_header access-control-allow-headers;
proxy_hide_header Access-Control-Allow-Origin;
add_header 'Access-Control-Allow-Origin' $http_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'sessionid, Content-Type, Authorization' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
nginx.ingress.kubernetes.io/upstream-hash-by: $sessionid
{{- with .Values.ingress.annotations }}

View file

@ -59,7 +59,7 @@ spec:
- name: sourcemaps_reader
value: "http://sourcemapreader-openreplay.{{.Release.Namespace}}.{{.Values.global.clusterDomain}}:9000/%s/sourcemaps"
- name: ASSIST_URL
value: "http://assist-openreplay.{{.Release.Namespace}}.{{.Values.global.clusterDomain}}:9001/assist/%s"
value: {{ include "openreplay.assist_url" . }}
- name: ASSIST_JWT_SECRET
value: {{ .Values.global.assistJWTSecret }}
- name: JWT_SECRET

View file

@ -161,3 +161,11 @@ Create the volume mount config for redis TLS certificates
{{- printf "postgres://%s:$(pg_password)@%s:%s/%s" .Values.global.postgresql.postgresqlUser .Values.global.postgresql.postgresqlHost .Values.global.postgresql.postgresqlPort .Values.global.postgresql.postgresqlDatabase -}}
{{- end -}}
{{- end}}
{{- define "openreplay.assist_url"}}
{{- if .Values.global.enterpriseEditionLicense }}
{{- printf "http://assist-api-openreplay.%s.%s:9001/assist/%%s" .Release.Namespace .Values.global.clusterDomain }}
{{- else}}
{{- printf "http://assist-openreplay.%s.%s:9001/assist/%%s" .Release.Namespace .Values.global.clusterDomain }}
{{- end}}
{{- end}}

View file

@ -47,6 +47,8 @@ vault: &vault
{{- with secret "database/creds/db-app" -}}
POSTGRES_STRING=postgres://{{.Data.username}}:{{.Data.password}}@postgresql.db.svc.cluster.local:5432/postgres
{{- end -}}
assist-api:
enabled: false
minio:
# Force initialize minio, even if the instance is not provisioned by OR