diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3fd4f1a31..9e8c36fdb 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,5 +4,15 @@ FROM ghcr.io/chatwoot/chatwoot_codespace:latest # Do the set up required for chatwoot app WORKDIR /workspace + +# Copy dependency files first for better caching +COPY package.json pnpm-lock.yaml ./ +COPY Gemfile Gemfile.lock ./ + +# Install dependencies (will be cached if files don't change) +RUN pnpm install --frozen-lockfile && \ + gem install bundler && \ + bundle install --jobs=$(nproc) + +# Copy source code after dependencies are installed COPY . /workspace -RUN yarn && gem install bundler && bundle install diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index fe31dc42e..dc7d4eb8c 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -1,12 +1,16 @@ - -ARG VARIANT +ARG VARIANT="ubuntu-22.04" FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} +ENV DEBIAN_FRONTEND=noninteractive + ARG NODE_VERSION ARG RUBY_VERSION ARG USER_UID ARG USER_GID +ARG PNPM_VERSION="10.2.0" +ENV PNPM_VERSION ${PNPM_VERSION} +ENV RUBY_CONFIGURE_OPTS=--disable-install-doc # Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ @@ -15,61 +19,80 @@ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ && chmod -R $USER_UID:$USER_GID /home/vscode; \ fi -RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends \ - build-essential \ - libssl-dev \ - zlib1g-dev \ - gnupg2 \ - tar \ - tzdata \ - postgresql-client \ - libpq-dev \ - yarn \ - git \ - imagemagick \ - tmux \ - zsh \ - git-flow \ - npm \ - libyaml-dev +RUN NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1) \ + && curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \ + && apt-get update \ + && apt-get -y install --no-install-recommends \ + build-essential \ + libssl-dev \ + zlib1g-dev \ + gnupg \ + tar \ + tzdata \ + postgresql-client \ + libpq-dev \ + git \ + imagemagick \ + libyaml-dev \ + curl \ + ca-certificates \ + tmux \ + nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Install rbenv and ruby -RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ +# Install rbenv and ruby for root user first +RUN git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv \ && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ && echo 'eval "$(rbenv init -)"' >> ~/.bashrc ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH" -RUN git clone https://github.com/rbenv/ruby-build.git && \ +RUN git clone --depth 1 https://github.com/rbenv/ruby-build.git && \ PREFIX=/usr/local ./ruby-build/install.sh RUN rbenv install $RUBY_VERSION && \ rbenv global $RUBY_VERSION && \ rbenv versions -# Install overmind +# Set up rbenv for vscode user +RUN su - vscode -c "git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv" \ + && su - vscode -c "echo 'export PATH=\"\$HOME/.rbenv/bin:\$PATH\"' >> ~/.bashrc" \ + && su - vscode -c "echo 'eval \"\$(rbenv init -)\"' >> ~/.bashrc" \ + && su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv install $RUBY_VERSION" \ + && su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv global $RUBY_VERSION" + +# Install overmind and gh in single layer RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \ && gunzip overmind.gz \ - && sudo mv overmind /usr/local/bin \ - && chmod +x /usr/local/bin/overmind - - -# Install gh -RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && sudo apt update \ - && sudo apt install gh + && mv overmind /usr/local/bin \ + && chmod +x /usr/local/bin/overmind \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && apt-get update \ + && apt-get install -y --no-install-recommends gh \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Do the set up required for chatwoot app WORKDIR /workspace -COPY . /workspace +RUN chown vscode:vscode /workspace -# set up ruby -COPY Gemfile Gemfile.lock ./ -RUN gem install bundler && bundle install +# set up node js, pnpm and claude code in single layer +RUN npm install -g pnpm@${PNPM_VERSION} @anthropic-ai/claude-code \ + && npm cache clean --force -# set up node js -RUN npm install n -g && \ - n $NODE_VERSION -RUN npm install --global yarn -RUN yarn +# Switch to vscode user +USER vscode +ENV PATH="/home/vscode/.rbenv/bin:/home/vscode/.rbenv/shims:$PATH" + +# Copy dependency files first for better caching +COPY --chown=vscode:vscode Gemfile Gemfile.lock package.json pnpm-lock.yaml ./ + +# Install dependencies as vscode user +RUN eval "$(rbenv init -)" \ + && gem install bundler -N \ + && bundle install --jobs=$(nproc) \ + && pnpm install --frozen-lockfile + +# Copy source code after dependencies are installed +COPY --chown=vscode:vscode . /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d2dac356b..2e237bbcb 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,17 +4,26 @@ "dockerComposeFile": "docker-compose.yml", "settings": { - "terminal.integrated.shell.linux": "/bin/zsh" + "terminal.integrated.shell.linux": "/bin/zsh", + "extensions.showRecommendationsOnlyOnDemand": true, + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "search.exclude": { + "**/node_modules": true, + "**/tmp": true, + "**/log": true, + "**/coverage": true, + "**/public/packs": true + } }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "rebornix.Ruby", + "Shopify.ruby-lsp", "misogi.ruby-rubocop", - "wingrunr21.vscode-ruby", "davidpallinder.rails-test-runner", - "eamodio.gitlens", "github.copilot", "mrmlnc.vscode-duplicate" ], @@ -23,15 +32,15 @@ // 5432 postgres // 6379 redis // 1025,8025 mailhog - "forwardPorts": [8025, 3000, 3035], + "forwardPorts": [8025, 3000, 3036], - "postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && yarn", + "postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && pnpm install", "portsAttributes": { "3000": { "label": "Rails Server" }, - "3035": { - "label": "Webpack Dev Server" + "3036": { + "label": "Vite Dev Server" }, "8025": { "label": "Mailhog UI" diff --git a/.devcontainer/docker-compose.base.yml b/.devcontainer/docker-compose.base.yml new file mode 100644 index 000000000..6932b5f10 --- /dev/null +++ b/.devcontainer/docker-compose.base.yml @@ -0,0 +1,18 @@ +# Docker Compose file for building the base image in GitHub Actions +# Usage: docker-compose -f .devcontainer/docker-compose.base.yml build base + +version: '3' + +services: + base: + build: + context: .. + dockerfile: .devcontainer/Dockerfile.base + args: + VARIANT: 'ubuntu-22.04' + NODE_VERSION: '23.7.0' + RUBY_VERSION: '3.4.4' + # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. + USER_UID: '1000' + USER_GID: '1000' + image: ghcr.io/chatwoot/chatwoot_codespace:latest diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 21a9fe909..a9185ea09 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,19 +5,6 @@ version: '3' services: - base: - build: - context: .. - dockerfile: .devcontainer/Dockerfile.base - args: - VARIANT: 'ubuntu-22.04' - NODE_VERSION: '23.7.0' - RUBY_VERSION: '3.4.4' - # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. - USER_UID: '1000' - USER_GID: '1000' - image: base:latest - app: build: context: .. diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 4ffee2d3a..36db5cfd9 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -2,12 +2,15 @@ cp .env.example .env sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env -sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env -sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env -# uncomment the webpacker env variable -sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env -# fix the error with webpacker -echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc +sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.app.github.dev/" .env + +# Setup Claude Code API key if available +if [ -n "$CLAUDE_CODE_API_KEY" ]; then + mkdir -p ~/.claude + echo '{"apiKeyHelper": "~/.claude/anthropic_key.sh"}' > ~/.claude/settings.json + echo "echo \"$CLAUDE_CODE_API_KEY\"" > ~/.claude/anthropic_key.sh + chmod +x ~/.claude/anthropic_key.sh +fi # codespaces make the ports public -gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME +gh codespace ports visibility 3000:public 3036:public 8025:public -c $CODESPACE_NAME diff --git a/.github/workflows/publish_codespace_image.yml b/.github/workflows/publish_codespace_image.yml index 647608473..5da4fda05 100644 --- a/.github/workflows/publish_codespace_image.yml +++ b/.github/workflows/publish_codespace_image.yml @@ -19,6 +19,5 @@ jobs: - name: Build the Codespace Base Image run: | - docker-compose -f .devcontainer/docker-compose.yml build base - docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest + docker compose -f .devcontainer/docker-compose.base.yml build base docker push ghcr.io/chatwoot/chatwoot_codespace:latest diff --git a/Makefile b/Makefile index 1c5ce297c..b7a936dc4 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,7 @@ run: force_run: rm -f ./.overmind.sock + rm -f tmp/pids/*.pid overmind start -f Procfile.dev debug: diff --git a/app/controllers/api/v1/accounts/agent_bots_controller.rb b/app/controllers/api/v1/accounts/agent_bots_controller.rb index 1422beea1..64c35d33d 100644 --- a/app/controllers/api/v1/accounts/agent_bots_controller.rb +++ b/app/controllers/api/v1/accounts/agent_bots_controller.rb @@ -29,6 +29,11 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController head :ok end + def reset_access_token + @agent_bot.access_token.regenerate_token + @agent_bot.reload + end + private def agent_bot diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index ae1a1fe30..141253d0d 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -38,6 +38,11 @@ class Api::V1::ProfilesController < Api::BaseController head :ok end + def reset_access_token + @user.access_token.regenerate_token + @user.reload + end + private def set_user diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index d07dbcb9d..32a147d34 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -1,7 +1,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController before_action :ensure_custom_domain_request, only: [:show, :index] before_action :portal - before_action :set_category, except: [:index, :show] + before_action :set_category, except: [:index, :show, :tracking_pixel] before_action :set_article, only: [:show] layout 'portal' @@ -15,6 +15,21 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def show; end + def tracking_pixel + @article = @portal.articles.find_by(slug: permitted_params[:article_slug]) + return head :not_found unless @article + + @article.increment_view_count if @article.published? + + # Serve the 1x1 tracking pixel with 24-hour private cache + # Private cache bypasses CDN but allows browser caching to prevent duplicate views from same user + expires_in 24.hours, public: false + response.headers['Content-Type'] = 'image/png' + + pixel_path = Rails.public_path.join('assets/images/tracking-pixel.png') + send_file pixel_path, type: 'image/png', disposition: 'inline' + end + private def limit_results @@ -39,7 +54,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def set_article @article = @portal.articles.find_by(slug: permitted_params[:article_slug]) - @article.increment_view_count if @article.published? @parsed_content = render_article_content(@article.content) end diff --git a/app/javascript/dashboard/api/agentBots.js b/app/javascript/dashboard/api/agentBots.js index 6e59f38d3..de887f415 100644 --- a/app/javascript/dashboard/api/agentBots.js +++ b/app/javascript/dashboard/api/agentBots.js @@ -21,6 +21,10 @@ class AgentBotsAPI extends ApiClient { deleteAgentBotAvatar(botId) { return axios.delete(`${this.url}/${botId}/avatar`); } + + resetAccessToken(botId) { + return axios.post(`${this.url}/${botId}/reset_access_token`); + } } export default new AgentBotsAPI(); diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index dde817866..75e7e2953 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -102,4 +102,8 @@ export default { const urlData = endPoints('resendConfirmation'); return axios.post(urlData.url); }, + resetAccessToken() { + const urlData = endPoints('resetAccessToken'); + return axios.post(urlData.url); + }, }; diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index 31337b7fc..5409aac60 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -51,6 +51,9 @@ const endPoints = { resendConfirmation: { url: '/api/v1/profile/resend_confirmation', }, + resetAccessToken: { + url: '/api/v1/profile/reset_access_token', + }, }; export default page => { diff --git a/app/javascript/dashboard/api/specs/agentBots.spec.js b/app/javascript/dashboard/api/specs/agentBots.spec.js index c89dbfdf5..bf57804c0 100644 --- a/app/javascript/dashboard/api/specs/agentBots.spec.js +++ b/app/javascript/dashboard/api/specs/agentBots.spec.js @@ -9,5 +9,6 @@ describe('#AgentBotsAPI', () => { expect(AgentBotsAPI).toHaveProperty('create'); expect(AgentBotsAPI).toHaveProperty('update'); expect(AgentBotsAPI).toHaveProperty('delete'); + expect(AgentBotsAPI).toHaveProperty('resetAccessToken'); }); }); diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss index 9c4ccf126..afaaf91c8 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_base.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -101,7 +101,7 @@ select { background-image: url("data:image/svg+xml;utf8,"); background-size: 9px 6px; - @apply field-base h-10 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 ltr:bg-[right_-1rem_center] rtl:bg-[left_-1rem_center] ltr:pr-6 rtl:pl-6 rtl:pr-3 ltr:pl-3; + @apply field-base h-10 bg-origin-content bg-no-repeat py-2 ltr:bg-[right_-1rem_center] rtl:bg-[left_-1rem_center] ltr:pr-6 rtl:pl-6 rtl:pr-3 ltr:pl-3; &[disabled] { @apply field-disabled; diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index 72a2e6be8..72773de7f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -3,7 +3,7 @@ } .tabs--container--with-border { - @apply border-b border-n-weak; + @apply border-b border-b-n-weak; } .tabs--container--compact.tab--chat-type { diff --git a/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue b/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue new file mode 100644 index 000000000..ec3a8d03a --- /dev/null +++ b/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue @@ -0,0 +1,87 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue b/app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue similarity index 50% rename from app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue rename to app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue index 78a345093..13d528240 100644 --- a/app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue +++ b/app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue @@ -1,21 +1,29 @@ diff --git a/app/javascript/dashboard/components-next/SidebarActionsHeader.vue b/app/javascript/dashboard/components-next/SidebarActionsHeader.vue new file mode 100644 index 000000000..210ddfa0e --- /dev/null +++ b/app/javascript/dashboard/components-next/SidebarActionsHeader.vue @@ -0,0 +1,47 @@ + + + diff --git a/app/javascript/dashboard/components-next/button/ConfirmButton.story.vue b/app/javascript/dashboard/components-next/button/ConfirmButton.story.vue new file mode 100644 index 000000000..673661a74 --- /dev/null +++ b/app/javascript/dashboard/components-next/button/ConfirmButton.story.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/javascript/dashboard/components-next/button/ConfirmButton.vue b/app/javascript/dashboard/components-next/button/ConfirmButton.vue new file mode 100644 index 000000000..854d5d452 --- /dev/null +++ b/app/javascript/dashboard/components-next/button/ConfirmButton.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/app/javascript/dashboard/components-next/copilot/Copilot.vue b/app/javascript/dashboard/components-next/copilot/Copilot.vue index 5feb474a6..6fb45c278 100644 --- a/app/javascript/dashboard/components-next/copilot/Copilot.vue +++ b/app/javascript/dashboard/components-next/copilot/Copilot.vue @@ -3,13 +3,15 @@ import { nextTick, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useTrack } from 'dashboard/composables'; import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; +import { useUISettings } from 'dashboard/composables/useUISettings'; import CopilotInput from './CopilotInput.vue'; import CopilotLoader from './CopilotLoader.vue'; import CopilotAgentMessage from './CopilotAgentMessage.vue'; import CopilotAssistantMessage from './CopilotAssistantMessage.vue'; import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue'; -import Icon from '../icon/Icon.vue'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; +import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue'; const props = defineProps({ supportAgent: { @@ -54,10 +56,6 @@ const useSuggestion = opt => { useTrack(COPILOT_EVENTS.SEND_SUGGESTED); }; -const handleReset = () => { - emit('reset'); -}; - const chatContainer = ref(null); const scrollToBottom = async () => { @@ -82,6 +80,21 @@ const promptOptions = [ }, ]; +const { updateUISettings } = useUISettings(); + +const closeCopilotPanel = () => { + updateUISettings({ + is_copilot_panel_open: false, + is_contact_sidebar_open: false, + }); +}; + +const handleSidebarAction = action => { + if (action === 'reset') { + emit('reset'); + } +}; + watch( [() => props.messages, () => props.isCaptainTyping], () => { @@ -93,6 +106,18 @@ watch(